diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 322054a..497469d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3', '8.4'] name: PHP ${{ matrix.php }} steps: - uses: actions/checkout@v4 @@ -18,10 +18,11 @@ jobs: with: php-version: ${{ matrix.php }} extensions: posix, pcntl + env: + phpts: ts - name: Run tests run: | git clone https://github.com/markreedz/mrloop.git mrloop && \ git clone https://github.com/axboe/liburing.git liburing && cd liburing && make && sudo make install && \ - cd ../ && git clone https://github.com/h2o/picohttpparser.git picohttp && \ - phpize && ./configure --with-mrloop="$(pwd)/mrloop" --with-picohttp="$(pwd)/picohttp" && \ + cd ../ && phpize && ./configure --with-mrloop="$(pwd)/mrloop" && \ make && make test diff --git a/README.md b/README.md index 38fe3f3..17a1bfc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ PHP has, in recent years, seen an emergence of eventware built atop potent multi - Linux Kernel 5.4.1 or newer - [mrloop](https://github.com/markreedz/mrloop) - [liburing](https://github.com/axboe/liburing) -- [picohttpparser](https://github.com/h2o/picohttpparser) ## Installation @@ -20,10 +19,9 @@ It is important to have all the aforelisted requirements at the ready before att ```sh $ git clone https://github.com/ace411/mrloop.git -$ git clone https://github.com/h2o/picohttpparser.git $ git clone https://github.com/ringphp/php-mrloop.git $ cd -$ ./configure --with-mrloop= --with-picohttp= +$ phpize && ./configure --with-mrloop= $ make && sudo make install ``` @@ -64,8 +62,6 @@ class Mrloop callable $callback, ): void public writev(int|resource $fd, string $message): void - public static parseHttpRequest(string $request, int $headerlimit = 100): iterable - public static parseHttpResponse(string $response, int $headerlimit = 100): iterable public addTimer(float $interval, callable $callback): void public addPeriodicTimer(float $interval, callable $callback): void public futureTick(callable $callback): void @@ -80,8 +76,6 @@ class Mrloop - [`Mrloop::addWriteStream`](#mrloopaddwritestream) - [`Mrloop::tcpServer`](#mrlooptcpserver) - [`Mrloop::writev`](#mrloopwritev) -- [`Mrloop::parseHttpRequest`](#mrloopparsehttprequest) -- [`Mrloop::parseHttpResponse`](#mrloopparsehttpresponse) - [`Mrloop::addTimer`](#mrloopaddtimer) - [`Mrloop::addPeriodicTimer`](#mrloopaddperiodictimer) - [`Mrloop::futureTick`](#mrloopfuturetick) @@ -378,226 +372,6 @@ Listening on port 8080 ``` -### `Mrloop::parseHttpRequest` - -```php -public static Mrloop::parseHttpRequest( - string $request, - int $headerlimit = 100, -): iterable -``` - -Parses an HTTP request. - -> This is a function that utilizes the `picohttpparser` API. - -**Parameter(s)** - -- **request** (string) - The HTTP request to parse. -- **headerlimit** (int) - The number of headers to parse. - > The default limit is `100`. - -**Return value(s)** - -The parser will throw an exception in the event that an invalid HTTP request is encountered and will output a hashtable with the contents enumerated below otherwise. - -- **body** (string) - The request body. -- **headers** (iterable) - An associative array containing request headers. -- **method** (string) - The request method. -- **path** (string) - The request path. - -```php -use ringphp\Mrloop; - -$loop = Mrloop::init(); - -$loop->tcpServer( - 8080, - null, - null, - function (mixed ...$args) { - [$message,] = $args; - $response = static fn ( - string $message, - int $code = 200, - string $mime = 'text/plain', - ) => - \sprintf( - "HTTP/1.1 %d %s\r\ncontent-type: %s\r\ncontent-length: %d\r\n\r\n%s\r\n", - $code, - ($code === 200 ? 'OK' : 'Internal Server Error'), - $mime, - \strlen($message), - $message, - ); - - try { - $request = Mrloop::parseHttpRequest($message); - - return $response('Hello, user'); - } catch (\Throwable $err) { - return $response( - 'HTTP parser error', - 500, - ); - } - }, -); - -$loop->run(); -``` - -The example above will produce output similar to that in the snippet to follow. - -``` -Listening on port 8080 - -``` - -### `Mrloop::parseHttpResponse` - -```php -public static Mrloop::parseHttpResponse( - string $response, - int $headerlimit = 100, -): iterable -``` - -Parses an HTTP response. - -> This function also utilizes the `picohttpparser` API. - -**Parameter(s)** - -- **response** (string) - The HTTP response to parse. -- **headerlimit** (int) - The number of headers to parse. - > The default limit is `100`. - -**Return value(s)** - -The parser will throw an exception in the event that an invalid HTTP response is encountered and will output a hashtable with the contents enumerated below otherwise. - -- **body** (string) - The response body. -- **headers** (iterable) - An associative array containing response headers. -- **status** (int) - The response status code. -- **reason** (string) - The response reason phrase. - -```php -use ringphp\Mrloop; - -$loop = Mrloop::init(); - -$loop->addWriteStream( - $sock = \stream_socket_client('tcp://www.example.com:80'), - "GET / HTTP/1.0\r\nHost: www.example.com\r\nAccept: */*\r\n\r\n", - null, - function ($nbytes) use ($loop, $sock) { - $loop->addReadStream( - $sock, - null, - null, - null, - function ($data, $res) use ($sock, $loop) { - var_dump(Mrloop::parseHttpResponse($data)); - - \fclose($sock); - }, - ); - }, -); - -$loop->run(); -``` - -The example above will produce output similar to that in the snippet to follow. - -``` -array(4) { - ["reason"]=> - string(2) "OK" - ["status"]=> - int(200) - ["body"]=> - string(1256) " - - - Example Domain - - - - - - - - -
-

Example Domain

-

This domain is for use in illustrative examples in documents. You may use this - domain in literature without prior coordination or asking for permission.

-

More information...

-
- - -" - ["headers"]=> - array(13) { - ["Accept-Ranges"]=> - string(5) "bytes" - ["Age"]=> - string(6) "506325" - ["Cache-Control"]=> - string(14) "max-age=604800" - ["Content-Type"]=> - string(24) "text/html; charset=UTF-8" - ["Date"]=> - string(29) "Wed, 30 Oct 2024 15:37:43 GMT" - ["Etag"]=> - string(17) ""3147526947+gzip"" - ["Expires"]=> - string(29) "Wed, 06 Nov 2024 15:37:43 GMT" - ["Last-Modified"]=> - string(29) "Thu, 17 Oct 2019 07:18:26 GMT" - [""]=> - string(16) "ECAcc (dcd/7D5A)" - ["Vary"]=> - string(15) "Accept-Encoding" - ["X-Cache"]=> - string(3) "HIT" - ["Content-Length"]=> - string(4) "1256" - ["Connection"]=> - string(5) "close" - } -} - -``` - ### `Mrloop::addTimer` ```php diff --git a/config.m4 b/config.m4 index 96bd755..4507041 100644 --- a/config.m4 +++ b/config.m4 @@ -1,4 +1,4 @@ -dnl ext-mrloop config.m4 file +dnl mrloop extension for PHP (c) 2024 Lochemem Bruno Michael dnl PHP_ARG_ENABLE([mrloop], dnl [for mrloop support], @@ -11,12 +11,6 @@ PHP_ARG_WITH([mrloop], [specify path to mrloop library])], [no]) -PHP_ARG_WITH([picohttp], - [for picohttp library], - [AS_HELP_STRING([--with-picohttp], - [specify path to picohttp library])], - [no]) - if test "$PHP_MRLOOP" != "no"; then dnl add PHP version check PHP_VERSION=$($PHP_CONFIG --vernum) @@ -51,15 +45,7 @@ if test "$PHP_MRLOOP" != "no"; then AC_MSG_ERROR(Please download mrloop) fi - AC_MSG_CHECKING([for picohttpparser package]) - if test -s "$PHP_PICOHTTP/picohttpparser.c"; then - AC_MSG_RESULT(found picohttpparser package) - else - AC_MSG_RESULT(picohttpparser is not downloaded) - AC_MSG_ERROR(Please download picohttpparser) - fi - - CFLAGS="-g -O3 -luring -I$PHP_MRLOOP/ -I$PHP_PICOHTTP/" + CFLAGS="-g -O3 -luring -I$PHP_MRLOOP/" AC_DEFINE(HAVE_MRLOOP, 1, [ Have mrloop support ]) PHP_NEW_EXTENSION(mrloop, php_mrloop.c, $ext_shared) diff --git a/mrloop_arginfo.h b/mrloop_arginfo.h index 2b48693..20ba63f 100644 --- a/mrloop_arginfo.h +++ b/mrloop_arginfo.h @@ -25,16 +25,6 @@ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, nbytes, IS_LONG, 0, "null") ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Mrloop_parseHttpRequest, 0, 0, 2) -ZEND_ARG_TYPE_INFO(0, request, IS_STRING, 0) -ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, header_limit, IS_LONG, 0, "100") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Mrloop_parseHttpResponse, 0, 0, 2) -ZEND_ARG_TYPE_INFO(0, response, IS_STRING, 0) -ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, header_limit, IS_LONG, 0, "100") -ZEND_END_ARG_INFO() - ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Mrloop_addSignal, 0, 0, 2) ZEND_ARG_TYPE_INFO(0, signal, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) @@ -70,11 +60,9 @@ ZEND_METHOD(Mrloop, run); ZEND_METHOD(Mrloop, addTimer); ZEND_METHOD(Mrloop, addPeriodicTimer); ZEND_METHOD(Mrloop, tcpServer); -ZEND_METHOD(Mrloop, parseHttpRequest); ZEND_METHOD(Mrloop, addSignal); ZEND_METHOD(Mrloop, addReadStream); ZEND_METHOD(Mrloop, addWriteStream); -ZEND_METHOD(Mrloop, parseHttpResponse); ZEND_METHOD(Mrloop, writev); ZEND_METHOD(Mrloop, futureTick); @@ -85,11 +73,9 @@ static const zend_function_entry class_Mrloop_methods[] = { PHP_ME(Mrloop, addTimer, arginfo_class_Mrloop_addTimer, ZEND_ACC_PUBLIC) PHP_ME(Mrloop, addPeriodicTimer, arginfo_class_Mrloop_addPeriodicTimer, ZEND_ACC_PUBLIC) PHP_ME(Mrloop, tcpServer, arginfo_class_Mrloop_tcpServer, ZEND_ACC_PUBLIC) - PHP_ME(Mrloop, parseHttpRequest, arginfo_class_Mrloop_parseHttpRequest, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_ME(Mrloop, addSignal, arginfo_class_Mrloop_addSignal, ZEND_ACC_PUBLIC) - PHP_ME(Mrloop, addReadStream, arginfo_class_Mrloop_addReadStream, ZEND_ACC_PUBLIC) - PHP_ME(Mrloop, addWriteStream, arginfo_class_Mrloop_addWriteStream, ZEND_ACC_PUBLIC) - PHP_ME(Mrloop, parseHttpResponse, arginfo_class_Mrloop_parseHttpResponse, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) - PHP_ME(Mrloop, writev, arginfo_class_Mrloop_writev, ZEND_ACC_PUBLIC) - PHP_ME(Mrloop, futureTick, arginfo_class_Mrloop_futureTick, ZEND_ACC_PUBLIC) - PHP_FE_END}; + PHP_ME(Mrloop, addSignal, arginfo_class_Mrloop_addSignal, ZEND_ACC_PUBLIC) + PHP_ME(Mrloop, addReadStream, arginfo_class_Mrloop_addReadStream, ZEND_ACC_PUBLIC) + PHP_ME(Mrloop, addWriteStream, arginfo_class_Mrloop_addWriteStream, ZEND_ACC_PUBLIC) + PHP_ME(Mrloop, writev, arginfo_class_Mrloop_writev, ZEND_ACC_PUBLIC) + PHP_ME(Mrloop, futureTick, arginfo_class_Mrloop_futureTick, ZEND_ACC_PUBLIC) + PHP_FE_END}; diff --git a/php_mrloop.c b/php_mrloop.c index eb66f28..16168a9 100644 --- a/php_mrloop.c +++ b/php_mrloop.c @@ -49,20 +49,6 @@ PHP_METHOD(Mrloop, tcpServer) } /* }}} */ -/* {{{ proto array Mrloop::parseHttpRequest( string request [, int headerlimit = 100 ] ) */ -PHP_METHOD(Mrloop, parseHttpRequest) -{ - php_mrloop_parse_http_request(INTERNAL_FUNCTION_PARAM_PASSTHRU); -} -/* }}} */ - -/* {{{ proto array Mrloop::parseHttpResponse( string request [, int headerlimit = 100 ] ) */ -PHP_METHOD(Mrloop, parseHttpResponse) -{ - php_mrloop_parse_http_response(INTERNAL_FUNCTION_PARAM_PASSTHRU); -} -/* }}} */ - /* {{{ proto void Mrloop::addSignal( int signal [, callable callback ] ) */ PHP_METHOD(Mrloop, addSignal) { diff --git a/src/loop.c b/src/loop.c index e9b6d8f..682fcbf 100644 --- a/src/loop.c +++ b/src/loop.c @@ -205,7 +205,7 @@ static void php_mrloop_readv_cb(void *data, int res) iov = (php_iovec_t *)cb->data; char next[(size_t)iov->iov_len]; - sprintf(next, "%.*s", (int)iov->iov_len, (char *)iov->iov_base); + php_strncpy(next, iov->iov_base, iov->iov_len + 1); ZVAL_STRING(&args[0], next); ZVAL_LONG(&args[1], res); @@ -267,12 +267,12 @@ static void *php_mrloop_tcp_client_setup(int fd, char **buffer, int *bsize) socklen = sizeof(php_sockaddr_t); - if (getpeername(fd, &addr, &socklen) > -1) + if (getpeername(fd, (struct sockaddr *)&addr, &socklen) > -1) { inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN); - conn->addr = ecalloc(1, strlen(ip_str)); - strcpy(conn->addr, ip_str); + conn->addr = ecalloc(1, INET_ADDRSTRLEN); + php_strncpy(conn->addr, ip_str, INET_ADDRSTRLEN); conn->port = (size_t)addr.sin_port; } @@ -369,126 +369,6 @@ static void php_mrloop_tcp_server_listen(INTERNAL_FUNCTION_PARAMETERS) return; } -static void php_mrloop_parse_http_request(INTERNAL_FUNCTION_PARAMETERS) -{ - zend_string *request; - int http_parser, http_minor_version; - zval retval, retval_headers; - char *method, *path; - size_t header_count, method_len, path_len, buffer_len; - zend_long header_limit = DEFAULT_HTTP_HEADER_LIMIT; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_STR(request) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(header_limit) - ZEND_PARSE_PARAMETERS_END(); - - buffer_len = ZSTR_LEN(request); - phr_header_t headers[(size_t)header_limit]; - char buffer[(size_t)buffer_len]; - - header_count = sizeof(headers) / sizeof(headers[0]); - strcpy(buffer, ZSTR_VAL(request)); - - http_parser = phr_parse_request( - buffer, buffer_len, (const char **)&method, &method_len, (const char **)&path, - &path_len, &http_minor_version, headers, &header_count, 0); - - if (http_parser < 0) - { - PHP_MRLOOP_THROW("There is an error in the HTTP request syntax"); - RETURN_NULL(); - } - - array_init(&retval); - array_init(&retval_headers); - - char *body, tmp_method[method_len], tmp_path[path_len]; - body = buffer + http_parser; - - sprintf(tmp_method, "%.*s", (int)method_len, method); - sprintf(tmp_path, "%.*s", (int)path_len, path); - - add_assoc_string(&retval, "path", tmp_path); - add_assoc_string(&retval, "method", tmp_method); - add_assoc_string(&retval, "body", body); - - // initialize array for response headers - for (size_t idx = 0; idx < header_count; idx++) - { - if (headers[idx].name && headers[idx].value) - { - char tmp_header_name[headers[idx].name_len], tmp_header_value[headers[idx].value_len]; - sprintf(tmp_header_name, "%.*s", (int)headers[idx].name_len, headers[idx].name); - sprintf(tmp_header_value, "%.*s", (int)headers[idx].value_len, headers[idx].value); - - add_assoc_string(&retval_headers, tmp_header_name, tmp_header_value); - } - } - - add_assoc_zval(&retval, "headers", &retval_headers); - - RETURN_ZVAL(&retval, 0, 1); -} -static void php_mrloop_parse_http_response(INTERNAL_FUNCTION_PARAMETERS) -{ - zend_string *response; - int http_parser, http_minor_version, http_status; - zval retval, retval_headers; - char *message; - size_t header_count, message_len, buffer_len; - zend_long header_limit = DEFAULT_HTTP_HEADER_LIMIT; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_STR(response) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(header_limit) - ZEND_PARSE_PARAMETERS_END(); - - buffer_len = ZSTR_LEN(response); - phr_header_t headers[(size_t)header_limit]; - char buffer[(size_t)buffer_len]; - - header_count = sizeof(headers) / sizeof(headers[0]); - strcpy(buffer, ZSTR_VAL(response)); - - http_parser = phr_parse_response( - buffer, buffer_len, &http_minor_version, &http_status, (const char **)&message, - &message_len, headers, &header_count, 0); - - if (http_parser < 0) - { - PHP_MRLOOP_THROW("There is an error in the HTTP response syntax"); - RETURN_NULL(); - } - - array_init(&retval); - array_init(&retval_headers); - - char tmp_message[message_len], *body; - sprintf(tmp_message, "%.*s", (int)message_len, message); - body = buffer + http_parser; - - add_assoc_string(&retval, "reason", tmp_message); - add_assoc_long(&retval, "status", http_status); - add_assoc_string(&retval, "body", body); - - for (size_t idx = 0; idx < header_count; idx++) - { - if (headers[idx].name && headers[idx].value) - { - char tmp_header_name[headers[idx].name_len], tmp_header_value[headers[idx].value_len]; - sprintf(tmp_header_name, "%.*s", (int)headers[idx].name_len, headers[idx].name); - sprintf(tmp_header_value, "%.*s", (int)headers[idx].value_len, headers[idx].value); - - add_assoc_string(&retval_headers, tmp_header_name, tmp_header_value); - } - } - - add_assoc_zval(&retval, "headers", &retval_headers); - RETURN_ZVAL(&retval, 0, 1); -} static void php_mrloop_signal_cb(int sig) { @@ -637,7 +517,7 @@ static void php_mrloop_add_write_stream(INTERNAL_FUNCTION_PARAMETERS) iov->iov_base = ecalloc(1, nbytes); iov->iov_len = nbytes; - strcpy(iov->iov_base, ZSTR_VAL(contents)); + php_strncpy(iov->iov_base, ZSTR_VAL(contents), nbytes + 1); cb = emalloc(sizeof(php_mrloop_cb_t)); PHP_CB_TO_MRLOOP_CB(cb, fci, fci_cache); @@ -700,3 +580,30 @@ static void php_mrloop_writev(INTERNAL_FUNCTION_PARAMETERS) mr_writev(this->loop, fd, &iov, 1); mr_flush(this->loop); } + +static size_t php_strncpy(char *dst, char *src, size_t nbytes) +{ + const char *osrc = src; + size_t nleft = nbytes; + + // copy as many bytes as will fit + if (nleft != 0) + { + while (--nleft != 0) + { + if ((*dst++ = *src++) == '\0') + break; + } + } + + // not enough room in dst, add null byte and traverse rest of src + if (nleft == 0) + { + if (nbytes != 0) + *dst = '\0'; + while (*src++) + ; + } + + return (src - osrc - 1); +} diff --git a/src/loop.h b/src/loop.h index 74c7242..615d00f 100644 --- a/src/loop.h +++ b/src/loop.h @@ -11,7 +11,6 @@ #include "php.h" #include "php_network.h" #include "php_streams.h" -#include "picohttpparser.c" #include "signal.h" #include "sys/file.h" #include "zend_exceptions.h" @@ -116,6 +115,9 @@ ZEND_DECLARE_MODULE_GLOBALS(mrloop) static PHP_GINIT_FUNCTION(mrloop); +/* safe rendition of native C strncpy (adapted from https://github.com/ariadnavigo/strlcpy) */ +static size_t php_strncpy(char *dst, char *src, size_t nbytes); + /* creates mrloop object in PHP userspace */ static zend_object *php_mrloop_create_object(zend_class_entry *ce); /* frees PHP userspace-residing mrloop object */ @@ -150,10 +152,6 @@ static void *php_mrloop_tcp_client_setup(int fd, char **buffer, int *bsize); static int php_mrloop_tcp_server_recv(void *conn, int fd, ssize_t nbytes, char *buffer); /* starts a TCP server */ static void php_mrloop_tcp_server_listen(INTERNAL_FUNCTION_PARAMETERS); -/* parses an HTTP request */ -static void php_mrloop_parse_http_request(INTERNAL_FUNCTION_PARAMETERS); -/* parses an HTTP response */ -static void php_mrloop_parse_http_response(INTERNAL_FUNCTION_PARAMETERS); /* mrloop-bound callback specified during invocation of signal handlers */ static void php_mrloop_signal_cb(int sig); diff --git a/tests/004.phpt b/tests/004.phpt deleted file mode 100644 index 0665a9f..0000000 --- a/tests/004.phpt +++ /dev/null @@ -1,29 +0,0 @@ ---TEST-- -parseHttpRequest() parses HTTP request ---FILE-- - ---EXPECT-- -array(4) { - ["path"]=> - string(1) "/" - ["method"]=> - string(3) "GET" - ["body"]=> - string(0) "" - ["headers"]=> - array(2) { - ["host"]=> - string(14) "localhost:8080" - ["content-type"]=> - string(10) "text/plain" - } -} diff --git a/tests/005.phpt b/tests/005.phpt deleted file mode 100644 index 1521e40..0000000 --- a/tests/005.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -parseHttpRequest() throws exception when invalid HTTP request syntax is encountered ---FILE-- -getMessage(), - ); -} - -?> ---EXPECT-- -string(44) "There is an error in the HTTP request syntax" diff --git a/tests/006.phpt b/tests/006.phpt deleted file mode 100644 index 362148f..0000000 --- a/tests/006.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -parseHttpResponse() parses HTTP response ---FILE-- - ---EXPECT-- -array(4) { - ["reason"]=> - string(2) "OK" - ["status"]=> - int(200) - ["body"]=> - string(11) "Hello, user" - ["headers"]=> - array(2) { - ["server"]=> - string(6) "mrloop" - ["content-type"]=> - string(10) "text/plain" - } -} diff --git a/tests/007.phpt b/tests/007.phpt deleted file mode 100644 index fe4f1f2..0000000 --- a/tests/007.phpt +++ /dev/null @@ -1,20 +0,0 @@ ---TEST-- -parseHttpResponse() throws exception when invalid HTTP response syntax is encountered ---FILE-- -getMessage(), - ); -} - -?> ---EXPECT-- -string(45) "There is an error in the HTTP response syntax"