/** * sesimos - secure, simple, modern web server * @brief FastCGI interface implementation * @file src/lib/fastcgi.c * @author Lorenz Stechauner * @date 2020-12-26 */ #include "../defs.h" #include "fastcgi.h" #include "utils.h" #include "../logger.h" #include "list.h" #include "../workers.h" #include #include #include #include #include #include char *fastcgi_add_param(char *buf, const char *key, const char *value) { char *ptr = buf; unsigned long key_len = strlen(key); unsigned long val_len = strlen(value); if (key_len <= 127) { ptr[0] = (char) (key_len & 0x7F); ptr++; } else { *((int *) ptr) = htonl(0x80000000 | key_len); ptr += 4; } if (val_len <= 127) { ptr[0] = (char) (val_len & 0x7F); ptr++; } else { *((int *) ptr) = htonl(0x80000000 | val_len); ptr += 4; } memcpy(ptr, key, key_len); ptr += key_len; memcpy(ptr, value, val_len); ptr += val_len; return ptr; } int fastcgi_send_data(fastcgi_cnx_t *cnx, unsigned char type, unsigned short len, void *data) { // build header FCGI_Header header = { .version = FCGI_VERSION_1, .type = type, .requestId = htons(cnx->req_id), .contentLength = htons(len), .paddingLength = 0, .reserved = 0, }; // send FastCGI header with MSG_MORE flag if (sock_send_x(&cnx->socket, &header, sizeof(header), (len != 0) ? MSG_MORE : 0) == -1) { error("Unable to send to FastCGI socket"); return -1; } // send data (if available) if (sock_send_x(&cnx->socket, data, len, 0) == -1) { error("Unable to send to FastCGI socket"); return -1; } // return bytes sent totally return len + (int) sizeof(header); } int fastcgi_init(fastcgi_cnx_t *conn, int mode, unsigned int req_num, const sock *client, const http_req *req, const http_uri *uri) { conn->mode = mode; conn->header_sent = 0; conn->req_id = (req_num + 1) & 0xFFFF; conn->webroot = uri->webroot; conn->err = NULL; conn->fd_err_bytes = 0; conn->fd_out = -1; conn->fd_err = -1; sock_init(&conn->out, -1, SOCK_PIPE); conn->socket.enc = 0; if ((conn->socket.socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { error("Unable to create unix socket"); return -1; } struct sockaddr_un sock_addr = { AF_UNIX }; if (conn->mode == FASTCGI_BACKEND_PHP) { strcpy(sock_addr.sun_path, PHP_FPM_SOCKET); } if (connect(conn->socket.socket, (struct sockaddr *) &sock_addr, sizeof(sock_addr)) != 0) { error("Unable to connect to FastCGI (unix) socket"); return -1; } FCGI_BeginRequestBody begin = { .role = htons(FCGI_RESPONDER), .flags = 0, .reserved = {0}, }; if (fastcgi_send_data(conn, FCGI_BEGIN_REQUEST, sizeof(begin), &begin) == -1) return -1; char param_buf[4096], buf0[256], *param_ptr = param_buf; param_ptr = fastcgi_add_param(param_ptr, "REDIRECT_STATUS", "CGI"); param_ptr = fastcgi_add_param(param_ptr, "DOCUMENT_ROOT", uri->webroot); param_ptr = fastcgi_add_param(param_ptr, "GATEWAY_INTERFACE", "CGI/1.1"); param_ptr = fastcgi_add_param(param_ptr, "SERVER_SOFTWARE", SERVER_STR); param_ptr = fastcgi_add_param(param_ptr, "SERVER_PROTOCOL", "HTTP/1.1"); param_ptr = fastcgi_add_param(param_ptr, "SERVER_NAME", http_get_header_field(&req->hdr, "Host")); if (client->enc) { param_ptr = fastcgi_add_param(param_ptr, "HTTPS", "on"); } struct sockaddr_storage addr_storage; struct sockaddr_in6 *addr; socklen_t len = sizeof(addr_storage); getsockname(client->socket, (struct sockaddr *) &addr_storage, &len); addr = (struct sockaddr_in6 *) &addr_storage; sprintf(buf0, "%i", ntohs(addr->sin6_port)); param_ptr = fastcgi_add_param(param_ptr, "SERVER_PORT", buf0); len = sizeof(addr_storage); getpeername(client->socket, (struct sockaddr *) &addr_storage, &len); addr = (struct sockaddr_in6 *) &addr_storage; sprintf(buf0, "%i", ntohs(addr->sin6_port)); param_ptr = fastcgi_add_param(param_ptr, "REMOTE_PORT", buf0); param_ptr = fastcgi_add_param(param_ptr, "REMOTE_ADDR", conn->r_addr); param_ptr = fastcgi_add_param(param_ptr, "REMOTE_HOST", conn->r_host != NULL ? conn->r_host : conn->r_addr); //param_ptr = fastcgi_add_param(param_ptr, "REMOTE_IDENT", ""); //param_ptr = fastcgi_add_param(param_ptr, "REMOTE_USER", ""); param_ptr = fastcgi_add_param(param_ptr, "REQUEST_METHOD", req->method); param_ptr = fastcgi_add_param(param_ptr, "REQUEST_URI", req->uri); param_ptr = fastcgi_add_param(param_ptr, "SCRIPT_NAME", uri->filename + strlen(uri->webroot)); param_ptr = fastcgi_add_param(param_ptr, "SCRIPT_FILENAME", uri->filename); //param_ptr = fastcgi_add_param(param_ptr, "PATH_TRANSLATED", uri->filename); param_ptr = fastcgi_add_param(param_ptr, "QUERY_STRING", uri->query != NULL ? uri->query : ""); if (uri->pathinfo != NULL && strlen(uri->pathinfo) > 0) { sprintf(buf0, "/%s", uri->pathinfo); } else { buf0[0] = 0; } param_ptr = fastcgi_add_param(param_ptr, "PATH_INFO", buf0); //param_ptr = fastcgi_add_param(param_ptr, "AUTH_TYPE", ""); const char *content_length = http_get_header_field(&req->hdr, "Content-Length"); param_ptr = fastcgi_add_param(param_ptr, "CONTENT_LENGTH", content_length != NULL ? content_length : ""); const char *content_type = http_get_header_field(&req->hdr, "Content-Type"); param_ptr = fastcgi_add_param(param_ptr, "CONTENT_TYPE", content_type != NULL ? content_type : ""); //if (conn->ctx->geoip[0] != 0) { // param_ptr = fastcgi_add_param(param_ptr, "REMOTE_INFO", conn->ctx->geoip); //} for (int i = 0; i < list_size(req->hdr.fields); i++) { const http_field *f = &req->hdr.fields[i]; const char *name = http_field_get_name(f); char *ptr = buf0; ptr += sprintf(ptr, "HTTP_"); for (int j = 0; j < strlen(name); j++, ptr++) { char ch = name[j]; if ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) { ch = ch; } else if (ch >= 'a' && ch <= 'z') { ch &= 0x5F; } else { ch = '_'; } ptr[0] = ch; ptr[1] = 0; } param_ptr = fastcgi_add_param(param_ptr, buf0, http_field_get_value(f)); } if (fastcgi_send_data(conn, FCGI_PARAMS, param_ptr - param_buf, param_buf) == -1) return -1; if (fastcgi_send_data(conn, FCGI_PARAMS, 0, NULL) == -1) return -1; int pipes[2][2]; if (pipe(pipes[0]) == -1 || pipe(pipes[1]) == -1) return -1; conn->fd_out = pipes[1][1]; conn->out.socket = pipes[1][0]; sock_set_timeout(&conn->out, FASTCGI_TIMEOUT); conn->fd_err = pipes[0][1]; conn->err = fdopen(pipes[0][0], "r"); return 0; } int fastcgi_close_cnx(fastcgi_cnx_t *cnx) { int e = errno; if (cnx->err) fclose(cnx->err); cnx->err = NULL; sock_close(&cnx->socket); sock_close(&cnx->out); if (cnx->fd_err != -1) close(cnx->fd_err); if (cnx->fd_out != -1) close(cnx->fd_out); cnx->fd_err = -1; cnx->fd_out = -1; errno = e; return 0; } int fastcgi_close_stdin(fastcgi_cnx_t *cnx) { return (fastcgi_send_data(cnx, FCGI_STDIN, 0, NULL) == -1) ? -1 : 0; } int fastcgi_php_error(fastcgi_cnx_t *cnx, char *err_msg) { char *line = NULL, *line_ptr = NULL, *next_ptr = NULL; size_t line_len = 0; int err = 0; log_lvl_t msg_type = LOG_INFO; for (long ret; cnx->fd_err_bytes > 0 && (ret = getline(&line, &line_len, cnx->err)) != -1; cnx->fd_err_bytes -= ret) { if (ret > 0) line[ret - 1] = 0; line_ptr = line; if (strstarts(line_ptr, "PHP message: ")) { line_ptr += 13; } else if (!strstr(line_ptr, "; PHP message: ")) { logmsgf(msg_type, "%s", line_ptr); continue; } while (line_ptr) { if (strstarts(line_ptr, "PHP Warning: ")) { msg_type = LOG_WARNING; } else if (strstarts(line_ptr, "PHP Fatal error: ")) { msg_type = LOG_ERROR; } else if (strstarts(line_ptr, "PHP Parse error: ")) { msg_type = LOG_ERROR; } else if (strstarts(line_ptr, "PHP Notice: ")) { msg_type = LOG_NOTICE; } else { msg_type = LOG_INFO; } if ((next_ptr = strstr(line_ptr, "; PHP message: "))) { next_ptr[0] = 0; } logmsgf(msg_type, "%s", line_ptr); if (err_msg && msg_type <= LOG_ERROR && line_ptr != line) { strcpy_rem_webroot(err_msg, line_ptr, cnx->webroot); err = 1; } line_ptr = next_ptr ? next_ptr + 15 : NULL; } } // cleanup free(line); return err; } int fastcgi_recv_frame(fastcgi_cnx_t *cnx) { FCGI_Header header; unsigned short req_id, content_len; if (sock_recv_x(&cnx->socket, &header, sizeof(header), 0) == -1) return -1; req_id = ntohs(header.requestId); content_len = ntohs(header.contentLength); if (req_id != cnx->req_id) { warning("Invalid request id from FastCGI socket"); char content[256 * 256]; sock_recv_x(&cnx->socket, content, content_len + header.paddingLength, 0); return -1; } if (header.type == FCGI_STDOUT || header.type == FCGI_STDERR) { char buf[256]; if (header.type == FCGI_STDOUT && !cnx->header_sent) { char content[256 * 256]; if (sock_recv_x(&cnx->socket, content, content_len + header.paddingLength, 0) == -1) return -1; char *h_pos = strstr(content, "\r\n\r\n"); long header_len = h_pos - content + 4; if (h_pos != NULL) { uint64_t len; len = header_len; if (write(cnx->fd_out, &len, sizeof(len)) == -1) return -1; if (write(cnx->fd_out, content, len) == -1) return -1; cnx->header_sent = 1; len = content_len - header_len; if (len > 0) { if (write(cnx->fd_out, &len, sizeof(len)) == -1) return -1; if (write(cnx->fd_out, content + header_len, len) == -1) return -1; } return header.type; } } else if (header.type == FCGI_STDOUT) { uint64_t len = content_len; if (write(cnx->fd_out, &len, sizeof(len)) == -1) return -1; } int fd = cnx->fd_out; if (header.type == FCGI_STDERR) { fd = cnx->fd_err; cnx->fd_err_bytes += content_len + 1; } for (long ret, sent = 0; sent < content_len; sent += ret) { // FIXME if pipe is full thread gets stuck if ((ret = splice(cnx->socket.socket, 0, fd, 0, content_len - sent, 0)) == -1) { if (errno == EINTR) { errno = 0, ret = 0; continue; } else { return -1; } } } if (header.type == FCGI_STDERR) write(fd, "\n", 1); if (sock_recv_x(&cnx->socket, buf, header.paddingLength, 0) == -1) return -1; return header.type; } char content[256 * 256]; if (sock_recv_x(&cnx->socket, content, content_len + header.paddingLength, 0) == -1) return -1; if (header.type == FCGI_END_REQUEST) { FCGI_EndRequestBody *body = (FCGI_EndRequestBody *) content; cnx->app_status = ntohl(body->appStatus); if (body->protocolStatus != FCGI_REQUEST_COMPLETE) error("FastCGI protocol error: %i", body->protocolStatus); } else { warning("Unknown FastCGI type: %i", header.type); return -1; } return header.type; } int fastcgi_header(fastcgi_cnx_t *cnx, http_res *res, char *err_msg) { long ret, len; char content[CLIENT_MAX_HEADER_SIZE]; if ((len = sock_recv_chunk_header(&cnx->out)) == -1) { res->status = http_get_status(500); sprintf(err_msg, "Unable to communicate with FastCGI socket."); error("Unable to receive from FastCGI socket (1)"); return -1; } if ((ret = sock_recv_x(&cnx->out, content, len, 0)) == -1) { res->status = http_get_status(500); sprintf(err_msg, "Unable to communicate with FastCGI socket."); error("Unable to receive from FastCGI socket (2)"); return -1; } content[ret] = 0; char *buf = content; char *h_pos = strstr(content, "\r\n\r\n"); if (h_pos == NULL) { error("Unable to parse header: End of header not found"); return -1; } long header_len = h_pos - content + 4; for (int i = 0; i < header_len; i++) { if ((buf[i] >= 0x00 && buf[i] <= 0x1F && buf[i] != '\r' && buf[i] != '\n') || buf[i] == 0x7F) { error("Unable to parse header: Header contains illegal characters"); return -1; } } if (fastcgi_php_error(cnx, err_msg) != 0) { res->status = http_get_status(500); return 1; } char *ptr = buf; while (header_len != (ptr - buf)) { char *pos0 = strstr(ptr, "\r\n"); if (pos0 == NULL) { error("Unable to parse header: Invalid header format"); return -1; } ret = http_parse_header_field(&res->hdr, ptr, pos0, 0); if (ret != 0) return (int) ret; if (pos0[2] == '\r' && pos0[3] == '\n') { return 0; } ptr = pos0 + 2; } return 0; } int fastcgi_dump(fastcgi_cnx_t *cnx, char *buf, const long len) { for (long ret, rcv = 0; rcv < len; rcv += ret) { if ((ret = sock_recv_chunk_header(&cnx->out)) == -1) { return -1; } const long min = ret > len - rcv ? len - rcv : ret; if ((ret = sock_recv_x(&cnx->out, buf + rcv, min, 0)) <= 0) { return -1; } } return 0; } int fastcgi_receive(fastcgi_cnx_t *cnx, sock *client, unsigned long len) { char buf[CHUNK_SIZE]; for (long to_send = (long) len, ret; to_send > 0; to_send -= ret) { if ((ret = sock_recv(client, buf, (to_send > sizeof(buf)) ? sizeof(buf) : to_send, 0)) <= 0) { error("Unable to receive"); return -1; } if (fastcgi_send_data(cnx, FCGI_STDIN, ret, buf) == -1) return -1; } return 0; } int fastcgi_receive_chunked(fastcgi_cnx_t *cnx, sock *client) { for (long ret;;) { if ((ret = sock_recv_chunk_header(client)) < 0) { return (int) ret; } else if (ret == 0) { break; } if ((ret = fastcgi_receive(cnx, client, ret)) < 0) return (int) ret; } return 0; }