Path: blob/main/crypto/openssl/apps/lib/http_server.c
34878 views
/*1* Copyright 1995-2024 The OpenSSL Project Authors. All Rights Reserved.2*3* Licensed under the Apache License 2.0 (the "License"). You may not use4* this file except in compliance with the License. You can obtain a copy5* in the file LICENSE in the source distribution or at6* https://www.openssl.org/source/license.html7*/89/* Very basic HTTP server */1011#if !defined(_POSIX_C_SOURCE) && defined(OPENSSL_SYS_VMS)12/*13* On VMS, you need to define this to get the declaration of fileno(). The14* value 2 is to make sure no function defined in POSIX-2 is left undefined.15*/16# define _POSIX_C_SOURCE 217#endif1819#include <ctype.h>20#include "internal/e_os.h"21#include "http_server.h"22#include "internal/sockets.h" /* for openssl_fdset() */2324#include <openssl/err.h>25#include <openssl/trace.h>26#include <openssl/rand.h>27#include "s_apps.h"28#include "log.h"2930#define HTTP_PREFIX "HTTP/"31#define HTTP_VERSION_PATT "1." /* allow 1.x */32#define HTTP_PREFIX_VERSION HTTP_PREFIX""HTTP_VERSION_PATT33#define HTTP_1_0 HTTP_PREFIX_VERSION"0" /* "HTTP/1.0" */34#define HTTP_VERSION_STR " "HTTP_PREFIX_VERSION3536#define log_HTTP(prog, level, text) \37trace_log_message(OSSL_TRACE_CATEGORY_HTTP, prog, level, "%s", text)38#define log_HTTP1(prog, level, fmt, arg) \39trace_log_message(OSSL_TRACE_CATEGORY_HTTP, prog, level, fmt, arg)40#define log_HTTP2(prog, level, fmt, arg1, arg2) \41trace_log_message(OSSL_TRACE_CATEGORY_HTTP, prog, level, fmt, arg1, arg2)42#define log_HTTP3(prog, level, fmt, a1, a2, a3) \43trace_log_message(OSSL_TRACE_CATEGORY_HTTP, prog, level, fmt, a1, a2, a3)4445#ifdef HTTP_DAEMON46int n_responders = 0; /* run multiple responder processes, set by ocsp.c */47int acfd = (int)INVALID_SOCKET;4849void socket_timeout(int signum)50{51if (acfd != (int)INVALID_SOCKET)52(void)shutdown(acfd, SHUT_RD);53}5455static void killall(int ret, pid_t *kidpids)56{57int i;5859for (i = 0; i < n_responders; ++i)60if (kidpids[i] != 0)61(void)kill(kidpids[i], SIGTERM);62OPENSSL_free(kidpids);63OSSL_sleep(1000);64exit(ret);65}6667static int termsig = 0;6869static void noteterm(int sig)70{71termsig = sig;72}7374/*75* Loop spawning up to `multi` child processes, only child processes return76* from this function. The parent process loops until receiving a termination77* signal, kills extant children and exits without returning.78*/79void spawn_loop(const char *prog)80{81pid_t *kidpids = NULL;82int status;83int procs = 0;84int i;8586openlog(prog, LOG_PID, LOG_DAEMON);8788if (setpgid(0, 0)) {89log_HTTP1(prog, LOG_CRIT,90"error detaching from parent process group: %s",91strerror(errno));92exit(1);93}94kidpids = app_malloc(n_responders * sizeof(*kidpids), "child PID array");95for (i = 0; i < n_responders; ++i)96kidpids[i] = 0;9798signal(SIGINT, noteterm);99signal(SIGTERM, noteterm);100101while (termsig == 0) {102pid_t fpid;103104/*105* Wait for a child to replace when we're at the limit.106* Slow down if a child exited abnormally or waitpid() < 0107*/108while (termsig == 0 && procs >= n_responders) {109if ((fpid = waitpid(-1, &status, 0)) > 0) {110for (i = 0; i < procs; ++i) {111if (kidpids[i] == fpid) {112kidpids[i] = 0;113--procs;114break;115}116}117if (i >= n_responders) {118log_HTTP1(prog, LOG_CRIT,119"internal error: no matching child slot for pid: %ld",120(long)fpid);121killall(1, kidpids);122}123if (status != 0) {124if (WIFEXITED(status)) {125log_HTTP2(prog, LOG_WARNING,126"child process: %ld, exit status: %d",127(long)fpid, WEXITSTATUS(status));128} else if (WIFSIGNALED(status)) {129char *dumped = "";130131# ifdef WCOREDUMP132if (WCOREDUMP(status))133dumped = " (core dumped)";134# endif135log_HTTP3(prog, LOG_WARNING,136"child process: %ld, term signal %d%s",137(long)fpid, WTERMSIG(status), dumped);138}139OSSL_sleep(1000);140}141break;142} else if (errno != EINTR) {143log_HTTP1(prog, LOG_CRIT,144"waitpid() failed: %s", strerror(errno));145killall(1, kidpids);146}147}148if (termsig)149break;150151switch (fpid = fork()) {152case -1: /* error */153/* System critically low on memory, pause and try again later */154OSSL_sleep(30000);155break;156case 0: /* child */157OPENSSL_free(kidpids);158signal(SIGINT, SIG_DFL);159signal(SIGTERM, SIG_DFL);160if (termsig)161_exit(0);162if (RAND_poll() <= 0) {163log_HTTP(prog, LOG_CRIT, "RAND_poll() failed");164_exit(1);165}166return;167default: /* parent */168for (i = 0; i < n_responders; ++i) {169if (kidpids[i] == 0) {170kidpids[i] = fpid;171procs++;172break;173}174}175if (i >= n_responders) {176log_HTTP(prog, LOG_CRIT,177"internal error: no free child slots");178killall(1, kidpids);179}180break;181}182}183184/* The loop above can only break on termsig */185log_HTTP1(prog, LOG_INFO, "terminating on signal: %d", termsig);186killall(0, kidpids);187}188#endif189190#ifndef OPENSSL_NO_SOCK191BIO *http_server_init(const char *prog, const char *port, int verb)192{193BIO *acbio = NULL, *bufbio;194int asock;195int port_num;196char name[40];197198BIO_snprintf(name, sizeof(name), "*:%s", port); /* port may be "0" */199if (verb >= 0 && !log_set_verbosity(prog, verb))200return NULL;201bufbio = BIO_new(BIO_f_buffer());202if (bufbio == NULL)203goto err;204acbio = BIO_new(BIO_s_accept());205if (acbio == NULL206|| BIO_set_accept_ip_family(acbio, BIO_FAMILY_IPANY) <= 0 /* IPv4/6 */207|| BIO_set_bind_mode(acbio, BIO_BIND_REUSEADDR) <= 0208|| BIO_set_accept_name(acbio, name) <= 0) {209log_HTTP(prog, LOG_ERR, "error setting up accept BIO");210goto err;211}212213BIO_set_accept_bios(acbio, bufbio);214bufbio = NULL;215if (BIO_do_accept(acbio) <= 0) {216log_HTTP1(prog, LOG_ERR, "error setting accept on port %s", port);217goto err;218}219220/* Report back what address and port are used */221BIO_get_fd(acbio, &asock);222port_num = report_server_accept(bio_out, asock, 1, 1);223if (port_num == 0) {224log_HTTP(prog, LOG_ERR, "error printing ACCEPT string");225goto err;226}227228return acbio;229230err:231ERR_print_errors(bio_err);232BIO_free_all(acbio);233BIO_free(bufbio);234return NULL;235}236237/*238* Decode %xx URL-decoding in-place. Ignores malformed sequences.239*/240static int urldecode(char *p)241{242unsigned char *out = (unsigned char *)p;243unsigned char *save = out;244245for (; *p; p++) {246if (*p != '%') {247*out++ = *p;248} else if (isxdigit(_UC(p[1])) && isxdigit(_UC(p[2]))) {249/* Don't check, can't fail because of ixdigit() call. */250*out++ = (OPENSSL_hexchar2int(p[1]) << 4)251| OPENSSL_hexchar2int(p[2]);252p += 2;253} else {254return -1;255}256}257*out = '\0';258return (int)(out - save);259}260261/* if *pcbio != NULL, continue given connected session, else accept new */262/* if found_keep_alive != NULL, return this way connection persistence state */263int http_server_get_asn1_req(const ASN1_ITEM *it, ASN1_VALUE **preq,264char **ppath, BIO **pcbio, BIO *acbio,265int *found_keep_alive,266const char *prog, int accept_get, int timeout)267{268BIO *cbio = *pcbio, *getbio = NULL, *b64 = NULL;269int len;270char reqbuf[2048], inbuf[2048];271char *meth, *url, *end;272ASN1_VALUE *req;273int ret = 0;274275*preq = NULL;276if (ppath != NULL)277*ppath = NULL;278279if (cbio == NULL) {280char *port;281282get_sock_info_address(BIO_get_fd(acbio, NULL), NULL, &port);283if (port == NULL) {284log_HTTP(prog, LOG_ERR, "cannot get port listening on");285goto fatal;286}287log_HTTP1(prog, LOG_DEBUG,288"awaiting new connection on port %s ...", port);289OPENSSL_free(port);290291if (BIO_do_accept(acbio) <= 0)292/* Connection loss before accept() is routine, ignore silently */293return ret;294295*pcbio = cbio = BIO_pop(acbio);296} else {297log_HTTP(prog, LOG_DEBUG, "awaiting next request ...");298}299if (cbio == NULL) {300/* Cannot call http_server_send_status(..., cbio, ...) */301ret = -1;302goto out;303}304305# ifdef HTTP_DAEMON306if (timeout > 0) {307(void)BIO_get_fd(cbio, &acfd);308alarm(timeout);309}310# endif311312/* Read the request line. */313len = BIO_gets(cbio, reqbuf, sizeof(reqbuf));314if (len == 0)315return ret;316ret = 1;317if (len < 0) {318log_HTTP(prog, LOG_WARNING, "request line read error");319(void)http_server_send_status(prog, cbio, 400, "Bad Request");320goto out;321}322323if (((end = strchr(reqbuf, '\r')) != NULL && end[1] == '\n')324|| (end = strchr(reqbuf, '\n')) != NULL)325*end = '\0';326if (log_get_verbosity() < LOG_TRACE)327trace_log_message(-1, prog, LOG_INFO,328"received request, 1st line: %s", reqbuf);329log_HTTP(prog, LOG_TRACE, "received request header:");330log_HTTP1(prog, LOG_TRACE, "%s", reqbuf);331if (end == NULL) {332log_HTTP(prog, LOG_WARNING,333"cannot parse HTTP header: missing end of line");334(void)http_server_send_status(prog, cbio, 400, "Bad Request");335goto out;336}337338url = meth = reqbuf;339if ((accept_get && CHECK_AND_SKIP_PREFIX(url, "GET "))340|| CHECK_AND_SKIP_PREFIX(url, "POST ")) {341342/* Expecting (GET|POST) {sp} /URL {sp} HTTP/1.x */343url[-1] = '\0';344while (*url == ' ')345url++;346if (*url != '/') {347log_HTTP2(prog, LOG_WARNING,348"invalid %s -- URL does not begin with '/': %s",349meth, url);350(void)http_server_send_status(prog, cbio, 400, "Bad Request");351goto out;352}353url++;354355/* Splice off the HTTP version identifier. */356for (end = url; *end != '\0'; end++)357if (*end == ' ')358break;359if (!HAS_PREFIX(end, HTTP_VERSION_STR)) {360log_HTTP2(prog, LOG_WARNING,361"invalid %s -- bad HTTP/version string: %s",362meth, end + 1);363(void)http_server_send_status(prog, cbio, 400, "Bad Request");364goto out;365}366*end = '\0';367/* above HTTP 1.0, connection persistence is the default */368if (found_keep_alive != NULL)369*found_keep_alive = end[sizeof(HTTP_VERSION_STR) - 1] > '0';370371/*-372* Skip "GET / HTTP..." requests often used by load-balancers.373* 'url' was incremented above to point to the first byte *after*374* the leading slash, so in case 'GET / ' it is now an empty string.375*/376if (strlen(meth) == 3 && url[0] == '\0') {377(void)http_server_send_status(prog, cbio, 200, "OK");378goto out;379}380381len = urldecode(url);382if (len < 0) {383log_HTTP2(prog, LOG_WARNING,384"invalid %s request -- bad URL encoding: %s", meth, url);385(void)http_server_send_status(prog, cbio, 400, "Bad Request");386goto out;387}388if (strlen(meth) == 3) { /* GET */389if ((getbio = BIO_new_mem_buf(url, len)) == NULL390|| (b64 = BIO_new(BIO_f_base64())) == NULL) {391log_HTTP1(prog, LOG_ERR,392"could not allocate base64 bio with size = %d", len);393goto fatal;394}395BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);396getbio = BIO_push(b64, getbio);397}398} else {399log_HTTP2(prog, LOG_WARNING,400"HTTP request does not begin with %sPOST: %s",401accept_get ? "GET or " : "", reqbuf);402(void)http_server_send_status(prog, cbio, 400, "Bad Request");403goto out;404}405406/* chop any further/duplicate leading or trailing '/' */407while (*url == '/')408url++;409while (end >= url + 2 && end[-2] == '/' && end[-1] == '/')410end--;411*end = '\0';412413/* Read and skip past the headers. */414for (;;) {415char *key, *value;416417len = BIO_gets(cbio, inbuf, sizeof(inbuf));418if (len <= 0) {419log_HTTP(prog, LOG_WARNING, "error reading HTTP header");420(void)http_server_send_status(prog, cbio, 400, "Bad Request");421goto out;422}423424if (((end = strchr(inbuf, '\r')) != NULL && end[1] == '\n')425|| (end = strchr(inbuf, '\n')) != NULL)426*end = '\0';427log_HTTP1(prog, LOG_TRACE, "%s", *inbuf == '\0' ?428" " /* workaround for "" getting ignored */ : inbuf);429if (end == NULL) {430log_HTTP(prog, LOG_WARNING,431"error parsing HTTP header: missing end of line");432(void)http_server_send_status(prog, cbio, 400, "Bad Request");433goto out;434}435436if (inbuf[0] == '\0')437break;438439key = inbuf;440value = strchr(key, ':');441if (value == NULL) {442log_HTTP(prog, LOG_WARNING,443"error parsing HTTP header: missing ':'");444(void)http_server_send_status(prog, cbio, 400, "Bad Request");445goto out;446}447*(value++) = '\0';448while (*value == ' ')449value++;450/* https://tools.ietf.org/html/rfc7230#section-6.3 Persistence */451if (found_keep_alive != NULL452&& OPENSSL_strcasecmp(key, "Connection") == 0) {453if (OPENSSL_strcasecmp(value, "keep-alive") == 0)454*found_keep_alive = 1;455else if (OPENSSL_strcasecmp(value, "close") == 0)456*found_keep_alive = 0;457}458}459460# ifdef HTTP_DAEMON461/* Clear alarm before we close the client socket */462alarm(0);463timeout = 0;464# endif465466/* Try to read and parse request */467req = ASN1_item_d2i_bio(it, getbio != NULL ? getbio : cbio, NULL);468if (req == NULL) {469log_HTTP(prog, LOG_WARNING,470"error parsing DER-encoded request content");471(void)http_server_send_status(prog, cbio, 400, "Bad Request");472} else if (ppath != NULL && (*ppath = OPENSSL_strdup(url)) == NULL) {473log_HTTP1(prog, LOG_ERR,474"out of memory allocating %zu bytes", strlen(url) + 1);475ASN1_item_free(req, it);476goto fatal;477}478479*preq = req;480481out:482BIO_free_all(getbio);483# ifdef HTTP_DAEMON484if (timeout > 0)485alarm(0);486acfd = (int)INVALID_SOCKET;487# endif488return ret;489490fatal:491(void)http_server_send_status(prog, cbio, 500, "Internal Server Error");492if (ppath != NULL) {493OPENSSL_free(*ppath);494*ppath = NULL;495}496BIO_free_all(cbio);497*pcbio = NULL;498ret = -1;499goto out;500}501502/* assumes that cbio does not do an encoding that changes the output length */503int http_server_send_asn1_resp(const char *prog, BIO *cbio, int keep_alive,504const char *content_type,505const ASN1_ITEM *it, const ASN1_VALUE *resp)506{507char buf[200], *p;508int ret = BIO_snprintf(buf, sizeof(buf), HTTP_1_0" 200 OK\r\n%s"509"Content-type: %s\r\n"510"Content-Length: %d\r\n",511keep_alive ? "Connection: keep-alive\r\n" : "",512content_type,513ASN1_item_i2d(resp, NULL, it));514515if (ret < 0 || (size_t)ret >= sizeof(buf))516return 0;517if (log_get_verbosity() < LOG_TRACE && (p = strchr(buf, '\r')) != NULL)518trace_log_message(-1, prog, LOG_INFO,519"sending response, 1st line: %.*s", (int)(p - buf),520buf);521log_HTTP1(prog, LOG_TRACE, "sending response header:\n%s", buf);522523ret = BIO_printf(cbio, "%s\r\n", buf) > 0524&& ASN1_item_i2d_bio(it, cbio, resp) > 0;525526(void)BIO_flush(cbio);527return ret;528}529530int http_server_send_status(const char *prog, BIO *cbio,531int status, const char *reason)532{533char buf[200];534int ret = BIO_snprintf(buf, sizeof(buf), HTTP_1_0" %d %s\r\n\r\n",535/* This implicitly cancels keep-alive */536status, reason);537538if (ret < 0 || (size_t)ret >= sizeof(buf))539return 0;540log_HTTP1(prog, LOG_TRACE, "sending response header:\n%s", buf);541542ret = BIO_printf(cbio, "%s\r\n", buf) > 0;543(void)BIO_flush(cbio);544return ret;545}546#endif547548549