From 4bc459a15cfc95a843e322664f38a87920050281 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 25 Mar 2026 01:08:25 +0000 Subject: [PATCH] Control API. A new nginx option -l specifies control socket listen address. The socket is providing an HTTP/1 REST API for retrieving information about running nginx instance. Example: $ nginx -l 127.0.0.1:1234 $ curl 127.0.0.1:1234/1/control/processes | jq [ { "pid": 41536, "name": "worker process", "exiting": false } ] Co-authored-by: Roman Arutyunyan Co-authored-by: Pavel Pautov Co-authored-by: Dmitry Plotnikov Co-authored-by: Sergey Kandaurov Co-authored-by: Ivan Ovchinnikov --- auto/modules | 17 + auto/options | 6 + auto/sources | 3 + src/core/nginx.c | 44 +- src/core/ngx_core.h | 3 + src/os/unix/ngx_control.c | 1257 +++++++++++++++++++++++++++++++ src/os/unix/ngx_control.h | 26 + src/os/unix/ngx_process_cycle.c | 20 + 8 files changed, 1375 insertions(+), 1 deletion(-) create mode 100644 src/os/unix/ngx_control.c create mode 100644 src/os/unix/ngx_control.h diff --git a/auto/modules b/auto/modules index f02860247..3082c8fde 100644 --- a/auto/modules +++ b/auto/modules @@ -56,6 +56,23 @@ if [ $NGX_TEST_BUILD_SOLARIS_SENDFILEV = YES ]; then fi +if [ $CONTROL_API = YES ]; then + + if [ "$NGX_PLATFORM" = win32 ]; then + cat << END + +$0: --with-control-api is not supported on Windows + +END + exit 1 + fi + + have=NGX_CONTROL_API . auto/have + CORE_DEPS="$CORE_DEPS $UNIX_CONTROL_API_DEPS" + CORE_SRCS="$CORE_SRCS $UNIX_CONTROL_API_SRCS" +fi + + if [ $HTTP = YES ]; then HTTP_MODULES= HTTP_DEPS= diff --git a/auto/options b/auto/options index 1aeb73074..332d74efb 100644 --- a/auto/options +++ b/auto/options @@ -41,6 +41,8 @@ EVENT_FOUND=NO EVENT_SELECT=NO EVENT_POLL=NO +CONTROL_API=NO + USE_THREADS=NO NGX_FILE_AIO=NO @@ -225,6 +227,8 @@ do --without-quic_bpf_module) QUIC_BPF=NONE ;; + --with-control-api) CONTROL_API=YES ;; + --with-ipv6) NGX_POST_CONF_MSG="$NGX_POST_CONF_MSG $0: warning: the \"--with-ipv6\" option is deprecated" @@ -468,6 +472,8 @@ cat << END --with-poll_module enable poll module --without-poll_module disable poll module + --with-control-api enable control API + --with-threads enable thread pool support --with-file-aio enable file AIO support diff --git a/auto/sources b/auto/sources index e5f1b44e4..00c8b05ad 100644 --- a/auto/sources +++ b/auto/sources @@ -192,6 +192,9 @@ UNIX_SRCS="$CORE_SRCS $EVENT_SRCS \ src/os/unix/ngx_dlopen.c \ src/os/unix/ngx_process_cycle.c" +UNIX_CONTROL_API_DEPS=src/os/unix/ngx_control.h +UNIX_CONTROL_API_SRCS=src/os/unix/ngx_control.c + POSIX_DEPS=src/os/unix/ngx_posix_config.h THREAD_POOL_MODULE=ngx_thread_pool_module diff --git a/src/core/nginx.c b/src/core/nginx.c index 0deb27b7f..5df9919ce 100644 --- a/src/core/nginx.c +++ b/src/core/nginx.c @@ -187,6 +187,9 @@ static u_char *ngx_prefix; static u_char *ngx_error_log; static u_char *ngx_conf_file; static u_char *ngx_conf_params; +#if (NGX_CONTROL_API) +static u_char *ngx_control_addr; +#endif static char *ngx_signal; @@ -360,6 +363,18 @@ main(int argc, char *const *argv) #endif +#if (NGX_CONTROL_API) + if (ngx_process == NGX_PROCESS_SINGLE) { + ngx_log_error(NGX_LOG_WARN, cycle->log, 0, + "control API unavailable in single process mode"); + + } else { + if (ngx_control_init(ngx_control_addr) != NGX_OK) { + return 1; + } + } +#endif + if (ngx_create_pidfile(&ccf->pid, cycle->log) != NGX_OK) { return 1; } @@ -410,6 +425,9 @@ ngx_show_version_info(void) "during configuration testing" NGX_LINEFEED " -s signal : send signal to a master process: " "stop, quit, reopen, reload" NGX_LINEFEED +#if (NGX_CONTROL_API) + " -l addr : open control socket on address" NGX_LINEFEED +#endif #ifdef NGX_PREFIX " -p prefix : set prefix path (default: " NGX_PREFIX ")" NGX_LINEFEED @@ -711,7 +729,7 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv) ctx.name = "new binary process"; ctx.argv = argv; - n = 2; + n = 3; env = ngx_set_environment(cycle, &n); if (env == NULL) { return NGX_INVALID_PID; @@ -751,6 +769,14 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv) #endif +#if (NGX_CONTROL_API) + env[n] = ngx_control_handoff(); + + if (env[n] != NULL) { + n++; + } +#endif + env[n] = NULL; #if (NGX_DEBUG) @@ -929,6 +955,22 @@ ngx_get_options(int argc, char *const *argv) ngx_log_stderr(0, "invalid option: \"-s %s\"", ngx_signal); return NGX_ERROR; +#if (NGX_CONTROL_API) + case 'l': + if (*p) { + ngx_control_addr = p; + goto next; + } + + if (argv[++i]) { + ngx_control_addr = (u_char *) argv[i]; + goto next; + } + + ngx_log_stderr(0, "option \"-l\" requires listen address"); + return NGX_ERROR; +#endif + default: ngx_log_stderr(0, "invalid option: \"%c\"", *(p - 1)); return NGX_ERROR; diff --git a/src/core/ngx_core.h b/src/core/ngx_core.h index d0226ac89..cbc4c028e 100644 --- a/src/core/ngx_core.h +++ b/src/core/ngx_core.h @@ -92,6 +92,9 @@ typedef void (*ngx_connection_handler_pt)(ngx_connection_t *c); #endif #endif #include +#if (NGX_CONTROL_API) +#include +#endif #include #include #include diff --git a/src/os/unix/ngx_control.c b/src/os/unix/ngx_control.c new file mode 100644 index 000000000..ddecfe640 --- /dev/null +++ b/src/os/unix/ngx_control.c @@ -0,0 +1,1257 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + + +#include +#include +#include + + +#define NGX_CTRL_MAX_FD 64 +#define NGX_CTRL_MAX_REQUEST 1024 + +#define NGX_CTRL_OK 200 +#define NGX_CTRL_BAD_REQUEST 400 +#define NGX_CTRL_NOT_FOUND 404 +#define NGX_CTRL_NOT_ALLOWED 405 +#define NGX_CTRL_UNPROCESSABLE 422 +#define NGX_CTRL_INTERNAL_ERROR 500 + +#define NGX_CTRL_GET 1 +#define NGX_CTRL_PATCH 2 + +#define NGX_CTRL_ENV "NGINX_CTRL" + + +typedef struct { + ngx_fd_t fd; + ngx_pool_t *pool; + ngx_uint_t index; + ngx_uint_t state; + + ngx_str_t path; + ngx_uint_t method; + + ngx_buf_t *in; + ngx_chain_t *out; +} ngx_control_request_t; + + +typedef struct { + ngx_str_t name; + ngx_int_t pid; + ngx_uint_t exiting; +} ngx_control_process_t; + + +static ngx_int_t ngx_control_inherit(void); +static void ngx_control_close(ngx_control_request_t *r); +static void ngx_control_handle_accept(void); +static ngx_int_t ngx_control_handle_read(ngx_control_request_t *r); +static ngx_int_t ngx_control_content(ngx_control_request_t *r); +static char *ngx_control_status_text(ngx_uint_t code); +static ngx_int_t ngx_control_send_response(ngx_control_request_t *r, + ngx_uint_t code, ngx_buf_t *body); +static ngx_int_t ngx_control_handle_write(ngx_control_request_t *r); + +static ngx_int_t ngx_control_api_endpoints(ngx_control_request_t *r, + const char **paths, ngx_int_t npaths); +static ngx_int_t ngx_control_show_processes(ngx_control_request_t *r); +static ngx_int_t ngx_control_show_version(ngx_control_request_t *r); + +static ngx_int_t ngx_control_parse_request_line(ngx_control_request_t *r); +static ngx_int_t ngx_control_send_json(ngx_control_request_t *r, ngx_int_t code, + ngx_data_item_t *obj); + + +static ngx_control_request_t ngx_control_requests[NGX_CTRL_MAX_FD]; +static struct pollfd ngx_control_pollfd[NGX_CTRL_MAX_FD]; +static ngx_uint_t ngx_control_pollfd_n; +static char ngx_control_unix_path[NGX_UNIX_ADDRSTRLEN]; +static ngx_uint_t ngx_control_inherited; +ngx_uint_t ngx_control_api_enabled; + + +static ngx_str_t ngx_control_version = ngx_string(NGINX_VERSION); + +#ifdef NGX_BUILD +static ngx_str_t ngx_control_build = ngx_string(NGX_BUILD); +#else +static ngx_str_t ngx_control_build; +#endif + +static ngx_data_decl_t ngx_control_process_fields[] = { + + { ngx_string("name"), + ngx_data_struct_str_handler, + offsetof(ngx_control_process_t, name) }, + + { ngx_string("pid"), + ngx_data_struct_int_handler, + offsetof(ngx_control_process_t, pid) }, + + { ngx_string("exiting"), + ngx_data_struct_boolean_handler, + offsetof(ngx_control_process_t, exiting) }, + + ngx_data_null_decl +}; + +static ngx_data_decl_t ngx_control_status_fields[] = { + + { ngx_string("version"), + ngx_data_string_handler, + (uintptr_t) &ngx_control_version }, + + { ngx_string("build"), + ngx_data_string_handler, + (uintptr_t) &ngx_control_build }, + + ngx_data_null_decl +}; + + +ngx_int_t +ngx_control_init(u_char *addr) +{ + int reuseaddr; + ngx_fd_t fd; + ngx_url_t u; + + if (addr == NULL) { + return NGX_OK; + } + + ngx_memzero(&u, sizeof(ngx_url_t)); + + u.url.data = addr; + u.url.len = ngx_strlen(addr); + u.listen = 1; + + if (ngx_parse_url(ngx_cycle->pool, &u) != NGX_OK) { + if (u.err) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, 0, + "control: %s in \"%V\"", u.err, &u.url); + } + + return NGX_ERROR; + } + +#if (NGX_HAVE_UNIX_DOMAIN) + if (u.family == AF_UNIX) { + ngx_memcpy(ngx_control_unix_path, u.host.data, u.host.len); + + } else if (u.no_port || u.last_port) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, 0, + "control: invalid port in \"%V\"", &u.url); + return NGX_ERROR; + } +#else + if (u.no_port || u.last_port) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, 0, + "control: invalid port in \"%V\"", &u.url); + return NGX_ERROR; + } +#endif + + if (ngx_control_inherit() == NGX_OK) { + ngx_control_inherited = 1; + return NGX_OK; + } + + fd = ngx_socket(u.family, SOCK_STREAM, 0); + + if (fd == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: " ngx_socket_n " failed"); + return NGX_ERROR; + } + + if (fcntl(fd, F_SETFL, O_ASYNC|O_NONBLOCK) == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: fcntl(O_ASYNC|O_NONBLOCK) failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + + if (fcntl(fd, F_SETOWN, ngx_pid) == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: fcntl(F_SETOWN) failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + + reuseaddr = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, + (const void *) &reuseaddr, sizeof(int)) + == -1) + { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: setsockopt(SO_REUSEADDR) failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + + if (bind(fd, &u.sockaddr.sockaddr, u.socklen) == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: bind() failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + +#if (NGX_HAVE_UNIX_DOMAIN) + if (u.family == AF_UNIX) { + if (chmod(ngx_control_unix_path, S_IRUSR|S_IWUSR) == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_errno, + "control: chmod() failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + } +#endif + + if (listen(fd, NGX_LISTEN_BACKLOG) == -1) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_socket_errno, + "control: listen() failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + + ngx_control_pollfd[0].fd = fd; + ngx_control_pollfd[0].events = POLLIN; + ngx_control_pollfd_n = 1; + + return NGX_OK; +} + + +void +ngx_control_uninit(void) +{ + ngx_control_close_sockets(); + + if (ngx_control_unix_path[0] && ngx_new_binary == 0 + && (!ngx_control_inherited || ngx_getppid() != ngx_parent)) + { + if (ngx_delete_file(ngx_control_unix_path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, ngx_cycle->log, ngx_errno, + ngx_delete_file_n " %s failed", ngx_control_unix_path); + } + } +} + + +void +ngx_control_close_sockets(void) +{ + ngx_uint_t i; + + for (i = 0; i < ngx_control_pollfd_n; i++) { + ngx_close_socket(ngx_control_pollfd[i].fd); + } +} + + +char * +ngx_control_handoff(void) +{ + static u_char env[sizeof(NGX_CTRL_ENV) + NGX_INT32_LEN + 1]; + + if (ngx_control_pollfd_n == 0) { + return NULL; + } + + ngx_sprintf(env, NGX_CTRL_ENV "=%d%Z", ngx_control_pollfd[0].fd); + + return (char *) env; +} + + +void +ngx_control_reown(void) +{ + if (ngx_control_pollfd_n == 0) { + return; + } + + if (fcntl(ngx_control_pollfd[0].fd, F_SETOWN, ngx_pid) == -1) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno, + "control: fcntl(F_SETOWN) failed"); + } +} + + +ngx_int_t +ngx_control_handle_events(void) +{ + int ready, revents; + ngx_uint_t i; + struct pollfd *pfd; + ngx_control_request_t *r; + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, + "control: handle events"); + + ready = poll(ngx_control_pollfd, ngx_control_pollfd_n, 0); + + if (ready <= 0) { + if (ready == -1 && ngx_errno != NGX_EINTR) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_errno, + "control: poll() failed"); + return NGX_ERROR; + } + + return NGX_OK; + } + + for (i = 0; i < ngx_control_pollfd_n; /* void */) { + + r = &ngx_control_requests[i]; + pfd = &ngx_control_pollfd[i]; + revents = pfd->revents; + + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, + "control: i:%ui fd:%d e:%d re:%d", + i, pfd->fd, pfd->events, pfd->revents); + + if ((revents & (POLLERR|POLLHUP|POLLNVAL)) && i != 0) { + ngx_control_close(r); + continue; + } + + if (revents & POLLIN) { + if (i == 0) { + ngx_control_handle_accept(); + i++; + continue; + } + + if (ngx_control_handle_read(r) != NGX_AGAIN) { + ngx_control_close(r); + continue; + } + } + + if (revents & POLLOUT) { + if (ngx_control_handle_write(r) != NGX_AGAIN) { + ngx_control_close(r); + continue; + } + } + + i++; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_control_inherit(void) +{ + u_char *env; + ngx_fd_t fd; + + env = (u_char *) getenv(NGX_CTRL_ENV); + if (env == NULL) { + return NGX_DECLINED; + } + + fd = ngx_atoi(env, ngx_strlen(env)); + if (fd == NGX_ERROR) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, 0, + "control: invalid " NGX_CTRL_ENV " value \"%s\"", env); + return NGX_ERROR; + } + + if (fcntl(fd, F_SETOWN, ngx_pid) == -1) { + ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, ngx_socket_errno, + "control: fcntl(F_SETOWN) failed"); + ngx_close_socket(fd); + return NGX_ERROR; + } + + ngx_control_pollfd[0].fd = fd; + ngx_control_pollfd[0].events = POLLIN; + ngx_control_pollfd_n = 1; + + return NGX_OK; +} + + +static void +ngx_control_close(ngx_control_request_t *r) +{ + ngx_uint_t i; + + i = r->index; + + ngx_close_socket(r->fd); + + ngx_destroy_pool(r->pool); + + if (i + 1 < ngx_control_pollfd_n) { + ngx_memmove(&ngx_control_pollfd[i], &ngx_control_pollfd[i + 1], + (ngx_control_pollfd_n - (i + 1)) * sizeof(struct pollfd)); + ngx_memmove(&ngx_control_requests[i], &ngx_control_requests[i + 1], + (ngx_control_pollfd_n - (i + 1)) * sizeof(ngx_control_request_t)); + + for (/* void */; i < ngx_control_pollfd_n - 1; i++) { + ngx_control_requests[i].index--; + } + } + + ngx_control_pollfd_n--; +} + + +static void +ngx_control_handle_accept(void) +{ + ngx_fd_t fd; + ngx_err_t err; + struct pollfd *pfd; + ngx_control_request_t *r; + + for ( ;; ) { + fd = accept(ngx_control_pollfd[0].fd, NULL, NULL); + + if (fd == -1) { + err = ngx_socket_errno; + + if (err == NGX_EAGAIN) { + return; + } + + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, err, + "control: accept() failed"); + + if (err == NGX_ECONNABORTED) { + continue; + } + + return; + } + + if (ngx_control_pollfd_n == NGX_CTRL_MAX_FD) { + ngx_log_error(NGX_LOG_WARN, ngx_cycle->log, 0, + "control: too many client connections"); + /* close the oldest connection */ + r = &ngx_control_requests[1]; + r->pool->log = ngx_cycle->log; + ngx_control_close(r); + } + + r = &ngx_control_requests[ngx_control_pollfd_n]; + ngx_memzero(r, sizeof(ngx_control_request_t)); + r->fd = fd; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, + "control accept fd:%d", r->fd); + + if (fcntl(r->fd, F_SETFD, FD_CLOEXEC) == -1) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "control: fcntl(FD_CLOEXEC) failed"); + ngx_close_socket(r->fd); + continue; + } + + if (fcntl(r->fd, F_SETFL, O_ASYNC|O_NONBLOCK) == -1) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "control: fcntl(O_ASYNC|O_NONBLOCK) failed"); + ngx_close_socket(r->fd); + continue; + } + + if (fcntl(r->fd, F_SETOWN, ngx_pid) == -1) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "control: fcntl(F_SETOWN) failed"); + ngx_close_socket(r->fd); + continue; + } + + pfd = &ngx_control_pollfd[ngx_control_pollfd_n]; + ngx_memzero(pfd, sizeof(struct pollfd)); + pfd->fd = r->fd; + pfd->events = POLLIN; + pfd->revents = POLLIN; + + r->pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, ngx_cycle->log); + if (r->pool == NULL) { + ngx_close_socket(r->fd); + continue; + } + + r->in = ngx_create_temp_buf(r->pool, NGX_CTRL_MAX_REQUEST); + if (r->in == NULL) { + ngx_destroy_pool(r->pool); + ngx_close_socket(r->fd); + continue; + } + + r->index = ngx_control_pollfd_n++; + } +} + + +static ngx_int_t +ngx_control_handle_read(ngx_control_request_t *r) +{ + ssize_t n; + ngx_buf_t *b; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, + "control: read fd:%d", r->fd); + + b = r->in; + + if (b->last == b->end) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "control: request too large"); + return NGX_ERROR; + } + + n = recv(r->fd, b->last, b->end - b->last, 0); + + if (n < 0) { + if (ngx_errno == NGX_EAGAIN) { + return NGX_AGAIN; + } + + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_socket_errno, + "control: recv() failed"); + return NGX_ERROR; + } + + if (n == 0) { + return NGX_DONE; + } + + b->last += n; + + switch (ngx_control_parse_request_line(r)) { + + case NGX_AGAIN: + return NGX_AGAIN; + + case NGX_DECLINED: + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, 0, + "control: failed to parse request"); + ngx_control_pollfd[r->index].events &= ~POLLIN; + return ngx_control_send_response(r, NGX_CTRL_BAD_REQUEST, NULL); + + case NGX_DONE: + ngx_control_pollfd[r->index].events &= ~POLLIN; + return ngx_control_content(r); + + default: + ngx_control_pollfd[r->index].events &= ~POLLIN; + return NGX_ERROR; + } +} + + +static ngx_int_t +ngx_control_content(ngx_control_request_t *r) +{ + u_char *p; + size_t len; + + static const char *root_endpoints[] = { "1" }; + static const char *api_endpoints[] = { "control", "nginx" }; + static const char *control_endpoints[] = { "processes" }; + + p = r->path.data; + len = r->path.len; + + if (len > 1 && p[len - 1] == '/') { + r->path.len = --len; + } + + /* the path always starts with "/" */ + + switch (len) { + case 1: + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + return ngx_control_api_endpoints(r, root_endpoints, 1); + + case 2: + if (ngx_strncmp(p, "/1", len) == 0) { + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + return ngx_control_api_endpoints(r, api_endpoints, 2); + } + + break; + + case 8: + if (ngx_strncmp(p, "/1/nginx", len) == 0) { + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + return ngx_control_show_version(r); + } + + break; + + case 10: + if (ngx_strncmp(p, "/1/control", len) == 0) { + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + return ngx_control_api_endpoints(r, control_endpoints, 1); + } + + break; + + case 20: + if (ngx_strncmp(p, "/1/control/processes", len) == 0) { + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + return ngx_control_show_processes(r); + } + + break; + } + + return ngx_control_send_response(r, NGX_CTRL_NOT_FOUND, NULL); +} + + +static char * +ngx_control_status_text(ngx_uint_t code) +{ + switch (code) { + case NGX_CTRL_OK: + return "OK"; + case NGX_CTRL_BAD_REQUEST: + return "Bad Request"; + case NGX_CTRL_NOT_FOUND: + return "Not Found"; + case NGX_CTRL_NOT_ALLOWED: + return "Not Allowed"; + case NGX_CTRL_UNPROCESSABLE: + return "Unprocessable Entity"; + case NGX_CTRL_INTERNAL_ERROR: + return "Internal Server Error"; + default: + return ""; + } +} + + +static ngx_int_t +ngx_control_send_response(ngx_control_request_t *r, ngx_uint_t code, + ngx_buf_t *body) +{ + char *status; + ngx_uint_t n; + ngx_chain_t *c; + + const char response_fmt[] = + "HTTP/1.1 %03ui %s\r\n" + "Server: " NGINX_VER "\r\n" + "Connection: close\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %ui\r\n" + "\r\n"; + + c = ngx_alloc_chain_link(r->pool); + if (c == NULL) { + return NGX_ERROR; + } + + status = ngx_control_status_text(code); + + c->buf = ngx_create_temp_buf(r->pool, + sizeof(response_fmt) + 2 * NGX_INT_T_LEN + ngx_strlen(status)); + if (c->buf == NULL) { + return NGX_ERROR; + } + + c->next = NULL; + + n = body ? body->last - body->pos : 0; + + c->buf->last = ngx_sprintf(c->buf->pos, response_fmt, code, status, n); + + if (body) { + c->next = ngx_alloc_chain_link(r->pool); + if (c->next == NULL) { + return NGX_ERROR; + } + + c->next->buf = body; + c->next->next = NULL; + } + + r->out = c; + + return ngx_control_handle_write(r); +} + + +static ngx_int_t +ngx_control_handle_write(ngx_control_request_t *r) +{ + ssize_t n; + ngx_buf_t *b; + ngx_chain_t *c; + + for (c = r->out; c != NULL; c = c->next) { + b = c->buf; + + while (b && b->pos < b->last) { + n = send(r->fd, b->pos, b->last - b->pos, 0); + + if (n <= 0) { + if (n == -1 && ngx_errno == NGX_EAGAIN) { + ngx_control_pollfd[r->index].events |= POLLOUT; + return NGX_AGAIN; + } + + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_socket_errno, + "control: send() failed"); + return NGX_ERROR; + } + + b->pos += n; + + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, ngx_cycle->log, 0, + "control: wrote %i bytes", n); + } + } + + return NGX_DONE; +} + + +static ngx_int_t +ngx_control_send_json(ngx_control_request_t *r, ngx_int_t code, + ngx_data_item_t *obj) +{ + ngx_buf_t *b; + + b = ngx_json_render(r->pool, obj); + if (b == NULL) { + return NGX_ERROR; + } + + return ngx_control_send_response(r, code, b); +} + + +static ngx_int_t +ngx_control_api_endpoints(ngx_control_request_t *r, const char **paths, + ngx_int_t npaths) +{ + ngx_int_t i; + ngx_str_t p; + ngx_data_item_t *json, *path; + + if (r->method != NGX_CTRL_GET) { + return ngx_control_send_response(r, NGX_CTRL_NOT_ALLOWED, NULL); + } + + json = ngx_data_new_list(r->pool); + if (json == NULL) { + return NGX_ERROR; + } + + for (i = 0; i < npaths; i++) { + p.data = (u_char *) paths[i]; + p.len = ngx_strlen(paths[i]); + + path = ngx_data_string_handler((uintptr_t) &p, r->pool, NULL); + if (path == NULL) { + return NGX_ERROR; + } + + ngx_data_add_item(json, NULL, path); + } + + return ngx_control_send_json(r, NGX_CTRL_OK, (ngx_data_item_t *) json); +} + + +static ngx_int_t +ngx_control_show_processes(ngx_control_request_t *r) +{ + ngx_int_t i; + ngx_data_item_t *process_list, *obj; + ngx_control_process_t process; + + process_list = ngx_data_new_list(r->pool); + if (process_list == NULL) { + return NGX_ERROR; + } + + for (i = 0; i < ngx_last_process; i++) { + if (ngx_processes[i].pid == -1) { + continue; + } + + process.name.data = (u_char *) ngx_processes[i].name; + process.name.len = ngx_strlen(process.name.data); + process.pid = ngx_processes[i].pid; + process.exiting = ngx_processes[i].exiting; + + obj = ngx_data_obj_handler((uintptr_t) ngx_control_process_fields, + r->pool, &process); + if (obj == NULL) { + return NGX_ERROR; + } + + ngx_data_add_item(process_list, NULL, obj); + } + + return ngx_control_send_json(r, NGX_CTRL_OK, process_list); +} + + +static ngx_int_t +ngx_control_show_version(ngx_control_request_t *r) +{ + ngx_data_item_t *resp; + + resp = ngx_data_obj_handler((uintptr_t) ngx_control_status_fields, r->pool, + NULL); + if (resp == NULL) { + return NGX_ERROR; + } + + return ngx_control_send_json(r, NGX_CTRL_OK, resp); +} + + +static ngx_int_t +ngx_control_parse_request_line(ngx_control_request_t *r) +{ + static uint32_t usual[] = { + 0x00000000, /* 0000 0000 0000 0000 0000 0000 0000 0000 */ + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x7fff37d6, /* 0111 1111 1111 1111 0011 0111 1101 0110 */ + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0x7fffffff, /* 0111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + }; + + u_char c, ch, *p; + ngx_buf_t *b; + enum { + sw_start = 0, + sw_method, + sw_spaces_before_uri, + sw_schema, + sw_schema_slash, + sw_schema_slash_slash, + sw_spaces_before_host, + sw_host_start, + sw_host, + sw_host_end, + sw_host_ip_literal, + sw_port_start, + sw_port, + sw_uri, + sw_http_, + sw_http_H, + sw_http_HT, + sw_http_HTT, + sw_http_HTTP, + sw_first_major_digit, + sw_major_digit, + sw_first_minor_digit, + sw_minor_digit, + sw_almost_done + } state; + + b = r->in; + state = r->state; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + /* HTTP methods: GET, PATCH */ + case sw_start: + if ((ch < 'A' || ch > 'Z') && ch != '_' && ch != '-') { + return NGX_DECLINED; + } + + state = sw_method; + + break; + + case sw_method: + if (ch == ' ') { + state = sw_spaces_before_uri; + + switch (p - b->start) { + case 3: + if (ngx_strncmp(b->start, "GET", 3) == 0) { + r->method = NGX_CTRL_GET; + break; + } + break; + + case 5: + if (ngx_strncmp(b->start, "PATCH", 5) == 0) { + r->method = NGX_CTRL_PATCH; + break; + } + break; + } + + break; + } + + if ((ch < 'A' || ch > 'Z') && ch != '_' && ch != '-') { + return NGX_DECLINED; + } + + break; + + /* space* before URI */ + case sw_spaces_before_uri: + if (ch == '/') { + r->path.data = p; + state = sw_uri; + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + state = sw_schema; + break; + } + + if (ch != ' ') { + return NGX_DECLINED; + } + + break; + + case sw_schema: + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.') + { + break; + } + + if (ch != ':') { + return NGX_DECLINED; + } + + state = sw_schema_slash; + break; + + case sw_schema_slash: + if (ch != '/') { + return NGX_DECLINED; + } + + state = sw_schema_slash_slash; + break; + + case sw_schema_slash_slash: + if (ch != '/') { + return NGX_DECLINED; + } + + state = sw_host_start; + break; + + case sw_spaces_before_host: + if (ch == ' ') { + break; + } + /* fall through */ + + case sw_host_start: + if (ch == '[') { + state = sw_host_ip_literal; + break; + } + + state = sw_host; + /* fall through */ + + case sw_host: + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { + break; + } + /* fall through */ + + case sw_host_end: + if (ch == ':') { + state = sw_port_start; + break; + } + + /* if we supported CONNECT verb we would need to decline here */ + + switch (ch) { + case '/': + r->path.data = p; + state = sw_uri; + break; + /* empty path cases */ + case '?': + ngx_str_set(&r->path, "/"); + state = sw_uri; + break; + case ' ': + ngx_str_set(&r->path, "/"); + state = sw_http_; + break; + } + break; + + case sw_host_ip_literal: + if (ch >= '0' && ch <= '9') { + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + switch (ch) { + case ':': + break; + case ']': + state = sw_host_end; + break; + case '-': + case '.': + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + default: + return NGX_DECLINED; + } + break; + + case sw_port_start: + state = sw_port; + + if (ch >= '0' && ch <= '9') { + break; + } + + /* if we supported CONNECT verb we would need to decline here */ + + /* fall through */ + + case sw_port: + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case '/': + r->path.data = p; + state = sw_uri; + break; + /* empty path cases */ + case '?': + ngx_str_set(&r->path, "/"); + state = sw_uri; + break; + case ' ': + ngx_str_set(&r->path, "/"); + state = sw_http_; + break; + default: + return NGX_DECLINED; + } + break; + + /* URI */ + case sw_uri: + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + break; + } + + switch (ch) { + case ' ': + state = sw_http_; + /* fall through */ + case '?': + if (r->path.len == 0) { + r->path.len = p - r->path.data; + } + break; + + case '.': + case '%': + case '#': + case '+': + case 0x7f: + return NGX_DECLINED; + default: + if (ch < 0x20) { + return NGX_DECLINED; + } + break; + } + break; + + /* space+ after URI */ + case sw_http_: + switch (ch) { + case ' ': + break; + case 'H': + state = sw_http_H; + break; + default: + return NGX_DECLINED; + } + break; + + case sw_http_H: + switch (ch) { + case 'T': + state = sw_http_HT; + break; + default: + return NGX_DECLINED; + } + break; + + case sw_http_HT: + switch (ch) { + case 'T': + state = sw_http_HTT; + break; + default: + return NGX_DECLINED; + } + break; + + case sw_http_HTT: + switch (ch) { + case 'P': + state = sw_http_HTTP; + break; + default: + return NGX_DECLINED; + } + break; + + case sw_http_HTTP: + switch (ch) { + case '/': + state = sw_first_major_digit; + break; + default: + return NGX_DECLINED; + } + break; + + /* first digit of major HTTP version */ + case sw_first_major_digit: + if (ch != '1') { + return NGX_DECLINED; + } + + state = sw_major_digit; + break; + + /* major HTTP version or dot */ + case sw_major_digit: + if (ch == '.') { + state = sw_first_minor_digit; + break; + } else { + /* major versions other than 1 are not supported */ + return NGX_DECLINED; + } + + /* first digit of minor HTTP version */ + case sw_first_minor_digit: + if (ch != '0' && ch != '1') { + return NGX_DECLINED; + } + + state = sw_minor_digit; + break; + + /* minor HTTP version or end of request line */ + case sw_minor_digit: + if (ch == CR) { + state = sw_almost_done; + break; + + } else if (ch == LF) { + goto done; + + } else { + /* support only 1.1 or 1.0 */ + return NGX_DECLINED; + } + + /* end of request line */ + case sw_almost_done: + switch (ch) { + case LF: + goto done; + default: + return NGX_DECLINED; + } + } + } + + b->pos = p; + r->state = state; + + return NGX_AGAIN; + +done: + b->pos = p + 1; + r->state = sw_start; + + return NGX_DONE; +} diff --git a/src/os/unix/ngx_control.h b/src/os/unix/ngx_control.h new file mode 100644 index 000000000..9b6c9b8dc --- /dev/null +++ b/src/os/unix/ngx_control.h @@ -0,0 +1,26 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + + +#ifndef _NGX_CONTROL_H_INCLUDED_ +#define _NGX_CONTROL_H_INCLUDED_ + + +#include +#include + + +ngx_int_t ngx_control_init(u_char *addr); +void ngx_control_uninit(void); +void ngx_control_close_sockets(void); +char *ngx_control_handoff(void); +void ngx_control_reown(void); +ngx_int_t ngx_control_handle_events(void); + + +extern ngx_uint_t ngx_control_api_enabled; + + +#endif /* _NGX_CONTROL_H_INCLUDED_ */ diff --git a/src/os/unix/ngx_process_cycle.c b/src/os/unix/ngx_process_cycle.c index 33c25487b..e3605ed03 100644 --- a/src/os/unix/ngx_process_cycle.c +++ b/src/os/unix/ngx_process_cycle.c @@ -174,6 +174,14 @@ ngx_master_process_cycle(ngx_cycle_t *cycle) live = ngx_reap_children(cycle); } +#if (NGX_CONTROL_API) + if (ngx_sigio) { + ngx_sigio = 0; + + ngx_control_handle_events(); + } +#endif + if (!live && (ngx_terminate || ngx_quit)) { ngx_master_process_exit(cycle); } @@ -629,6 +637,10 @@ ngx_reap_children(ngx_cycle_t *cycle) ccf->oldpid.data, ccf->pid.data, ngx_argv[0]); } +#if (NGX_CONTROL_API) + ngx_control_reown(); +#endif + ngx_new_binary = 0; if (ngx_noaccepting) { ngx_restart = 1; @@ -669,6 +681,10 @@ ngx_master_process_exit(ngx_cycle_t *cycle) ngx_close_listening_sockets(cycle); +#if (NGX_CONTROL_API) + ngx_control_uninit(); +#endif + /* * Copy ngx_cycle->log related data to the special static exit cycle, * log, and log file structures enough to allow a signal handler to log. @@ -760,6 +776,10 @@ ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker) struct rlimit rlmt; ngx_core_conf_t *ccf; +#if (NGX_CONTROL_API) + ngx_control_close_sockets(); +#endif + if (ngx_set_environment(cycle, NULL) == NULL) { /* fatal */ exit(2);