From 900bbe724ab87c863050a90a5b5b20d69b5c09d9 Mon Sep 17 00:00:00 2001 From: Marko Mikulicic Date: Wed, 26 Oct 2016 17:41:54 +0300 Subject: [PATCH] Mongoose forwarding PUBLISHED_FROM=51652f0157bb951a43508f0fe948c62c351e96ba --- docs/c-api/http_server.h/intro.md | 1 + .../c-api/http_server.h/mg_http_send_error.md | 11 ++ .../struct_mg_serve_http_opts.md | 12 +- examples/reverse_proxy/Makefile | 4 + examples/reverse_proxy/frontend/hello.html | 6 + examples/reverse_proxy/reverse_proxy.c | 127 +++++++++++++ mongoose.c | 177 +++++++++++++++--- mongoose.h | 24 ++- 8 files changed, 327 insertions(+), 35 deletions(-) create mode 100644 docs/c-api/http_server.h/mg_http_send_error.md create mode 100644 examples/reverse_proxy/Makefile create mode 100644 examples/reverse_proxy/frontend/hello.html create mode 100644 examples/reverse_proxy/reverse_proxy.c diff --git a/docs/c-api/http_server.h/intro.md b/docs/c-api/http_server.h/intro.md index 53adeb73..6e1c49f4 100644 --- a/docs/c-api/http_server.h/intro.md +++ b/docs/c-api/http_server.h/intro.md @@ -8,6 +8,7 @@ items: - { name: mg_get_http_var.md } - { name: mg_http_check_digest_auth.md } - { name: mg_http_parse_header.md } + - { name: mg_http_send_error.md } - { name: mg_http_send_redirect.md } - { name: mg_http_serve_file.md } - { name: mg_parse_http.md } diff --git a/docs/c-api/http_server.h/mg_http_send_error.md b/docs/c-api/http_server.h/mg_http_send_error.md new file mode 100644 index 00000000..d74aafc1 --- /dev/null +++ b/docs/c-api/http_server.h/mg_http_send_error.md @@ -0,0 +1,11 @@ +--- +title: "mg_http_send_error()" +decl_name: "mg_http_send_error" +symbol_kind: "func" +signature: | + void mg_http_send_error(struct mg_connection *nc, int code, const char *reason); +--- + +Sends an error response. If reason is NULL, the message will be inferred +from the error code (if supported). + diff --git a/docs/c-api/http_server.h/struct_mg_serve_http_opts.md b/docs/c-api/http_server.h/struct_mg_serve_http_opts.md index 74ad8529..ae594dba 100644 --- a/docs/c-api/http_server.h/struct_mg_serve_http_opts.md +++ b/docs/c-api/http_server.h/struct_mg_serve_http_opts.md @@ -100,16 +100,19 @@ signature: | /* IP ACL. By default, NULL, meaning all IPs are allowed to connect */ const char *ip_acl; + #if MG_ENABLE_HTTP_URL_REWRITES /* URL rewrites. * - * Comma-separated list of `uri_pattern=file_or_directory_path` rewrites. + * Comma-separated list of `uri_pattern=url_file_or_directory_path` rewrites. * When HTTP request is received, Mongoose constructs a file name from the * requested URI by combining `document_root` and the URI. However, if the * rewrite option is used and `uri_pattern` matches requested URI, then - * `document_root` is ignored. Instead, `file_or_directory_path` is used, + * `document_root` is ignored. Instead, `url_file_or_directory_path` is used, * which should be a full path name or a path relative to the web server's - * current working directory. Note that `uri_pattern`, as all Mongoose - * patterns, is a prefix pattern. + * current working directory. It can also be an URI (http:// or https://) + * in which case mongoose will behave as a reverse proxy for that destination. + * + * Note that `uri_pattern`, as all Mongoose patterns, is a prefix pattern. * * If uri_pattern starts with `@` symbol, then Mongoose compares it with the * HOST header of the request. If they are equal, Mongoose sets document root @@ -123,6 +126,7 @@ signature: | * automatically appended to the redirect location. */ const char *url_rewrites; + #endif /* DAV document root. If NULL, DAV requests are going to fail. */ const char *dav_document_root; diff --git a/examples/reverse_proxy/Makefile b/examples/reverse_proxy/Makefile new file mode 100644 index 00000000..df6a716a --- /dev/null +++ b/examples/reverse_proxy/Makefile @@ -0,0 +1,4 @@ +PROG = reverse_proxy +MODULE_CFLAGS = +SSL_LIB = +include ../examples.mk diff --git a/examples/reverse_proxy/frontend/hello.html b/examples/reverse_proxy/frontend/hello.html new file mode 100644 index 00000000..6384835e --- /dev/null +++ b/examples/reverse_proxy/frontend/hello.html @@ -0,0 +1,6 @@ + + + + Hello + + diff --git a/examples/reverse_proxy/reverse_proxy.c b/examples/reverse_proxy/reverse_proxy.c new file mode 100644 index 00000000..9863635f --- /dev/null +++ b/examples/reverse_proxy/reverse_proxy.c @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2014 Cesanta Software Limited + * All rights reserved + */ + +/* + * This example shows how mongoose can be used as a reverse + * proxy for another http server. + * + * A common setup is to have a frontend web server that delegates + * some urls to a backend web server. + * + * In this example we create two webservers. The frontend listens on port + * 8000 and servers a static file and forwards any call matching the /api prefix + * to the backend. + * + * The backend listens on port 8001 and replies a simple JSON object which + * shows the request URI that the backend http server receives. + * + * Things to try out: + * + * curl http://localhost:8000/ + * curl http://localhost:8000/api + * curl http://localhost:8000/api/foo + * curl http://localhost:8001/foo + * + * The reverse proxy functionality is enabled via the url rewrite functionality: + * + * ``` + * s_frontend_server_opts.url_rewrites = + * "/api=http://localhost:8001,/=frontend/hello.html"; + * ``` + * + * This example maps the /api to a remote http server, and / to a + * specific file on the filesystem. + * + * Obviously you can use any http server as the backend, we spawn + * another web server from the same process in order to make the example easy + * to run. + */ + +#include "../../mongoose.h" + +static const char *s_frontend_port = "8000"; +static struct mg_serve_http_opts s_frontend_server_opts; + +static const char *s_backend_port = "8001"; +static struct mg_serve_http_opts s_backend_server_opts; + +static void frontend_handler(struct mg_connection *nc, int ev, void *ev_data) { + struct http_message *hm = (struct http_message *) ev_data; + switch (ev) { + case MG_EV_HTTP_REQUEST: + mg_serve_http(nc, hm, s_frontend_server_opts); /* Serve static content */ + break; + default: + break; + } +} + +static void backend_handler(struct mg_connection *nc, int ev, void *ev_data) { + struct http_message *hm = (struct http_message *) ev_data; + int i; + + switch (ev) { + case MG_EV_HTTP_REQUEST: + mg_send_response_line(nc, 200, + "Content-Type: text/html\r\n" + "Connection: close\r\n"); + mg_printf(nc, + "{\"uri\": \"%.*s\", \"method\": \"%.*s\", \"body\": \"%.*s\", " + "\"headers\": {", + (int) hm->uri.len, hm->uri.p, (int) hm->method.len, + hm->method.p, (int) hm->body.len, hm->body.p); + + for (i = 0; i < MG_MAX_HTTP_HEADERS && hm->header_names[i].len > 0; i++) { + struct mg_str hn = hm->header_names[i]; + struct mg_str hv = hm->header_values[i]; + mg_printf(nc, "%s\"%.*s\": \"%.*s\"", (i != 0 ? "," : ""), (int) hn.len, + hn.p, (int) hv.len, hv.p); + } + + mg_printf(nc, "}}"); + + nc->flags |= MG_F_SEND_AND_CLOSE; + break; + default: + break; + } +} + +int main(int argc, char *argv[]) { + struct mg_mgr mgr; + struct mg_connection *nc; + int i; + + /* Open listening socket */ + mg_mgr_init(&mgr, NULL); + + /* configure frontend web server */ + nc = mg_bind(&mgr, s_frontend_port, frontend_handler); + mg_set_protocol_http_websocket(nc); + + s_frontend_server_opts.document_root = "frontend"; + s_frontend_server_opts.url_rewrites = + "/api=http://localhost:8001,/=frontend/hello.html"; + + /* configure backend web server */ + nc = mg_bind(&mgr, s_backend_port, backend_handler); + mg_set_protocol_http_websocket(nc); + + s_backend_server_opts.document_root = "backend"; + + /* Parse command line arguments */ + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-D") == 0) { + mgr.hexdump_file = argv[++i]; + } else if (strcmp(argv[i], "-r") == 0) { + s_frontend_server_opts.document_root = argv[++i]; + } + } + + printf("Starting web server on port %s\n", s_frontend_port); + for (;;) { + mg_mgr_poll(&mgr, 1000); + } +} diff --git a/mongoose.c b/mongoose.c index a5ffa8d7..d0e45954 100644 --- a/mongoose.c +++ b/mongoose.c @@ -1181,6 +1181,19 @@ int mg_strcmp(const struct mg_str str1, const struct mg_str str2) { if (i < str2.len) return -1; return 0; } + +int mg_strncmp(const struct mg_str str1, const struct mg_str str2, size_t n) { + struct mg_str s1 = str1; + struct mg_str s2 = str2; + + if (s1.len > n) { + s1.len = n; + } + if (s2.len > n) { + s2.len = n; + } + return mg_strcmp(s1, s2); +} #ifdef MG_MODULE_LINES #line 1 "common/sha1.c" #endif @@ -3970,6 +3983,10 @@ struct mg_http_proto_data { }; static void mg_http_conn_destructor(void *proto_data); +struct mg_connection *mg_connect_http_base( + struct mg_mgr *mgr, mg_event_handler_t ev_handler, + struct mg_connect_opts opts, const char *schema, const char *schema_ssl, + const char *url, const char **path, char **addr); static struct mg_http_proto_data *mg_http_get_proto_data( struct mg_connection *c) { @@ -5039,40 +5056,37 @@ void mg_set_protocol_http_websocket(struct mg_connection *nc) { nc->proto_handler = mg_http_handler; } -void mg_send_response_line_s(struct mg_connection *nc, int status_code, - const struct mg_str extra_headers) { - const char *status_message = "OK"; +const char *mg_status_message(int status_code) { switch (status_code) { case 206: - status_message = "Partial Content"; - break; + return "Partial Content"; case 301: - status_message = "Moved"; - break; + return "Moved"; case 302: - status_message = "Found"; - break; + return "Found"; case 401: - status_message = "Unauthorized"; - break; + return "Unauthorized"; case 403: - status_message = "Forbidden"; - break; + return "Forbidden"; case 404: - status_message = "Not Found"; - break; + return "Not Found"; case 416: - status_message = "Requested range not satisfiable"; - break; + return "Requested range not satisfiable"; case 418: - status_message = "I'm a teapot"; - break; + return "I'm a teapot"; case 500: - status_message = "Internal Server Error"; - break; + return "Internal Server Error"; + case 502: + return "Bad Gateway"; + default: + return "OK"; } - mg_printf(nc, "HTTP/1.1 %d %s\r\nServer: %s\r\n", status_code, status_message, - mg_version_header); +} + +void mg_send_response_line_s(struct mg_connection *nc, int status_code, + const struct mg_str extra_headers) { + mg_printf(nc, "HTTP/1.1 %d %s\r\nServer: %s\r\n", status_code, + mg_status_message(status_code), mg_version_header); if (extra_headers.len > 0) { mg_printf(nc, "%.*s\r\n", (int) extra_headers.len, extra_headers.p); } @@ -5116,10 +5130,9 @@ void mg_send_head(struct mg_connection *c, int status_code, mg_send(c, "\r\n", 2); } -#if MG_ENABLE_FILESYSTEM -static void mg_http_send_error(struct mg_connection *nc, int code, - const char *reason) { - if (!reason) reason = ""; +void mg_http_send_error(struct mg_connection *nc, int code, + const char *reason) { + if (!reason) reason = mg_status_message(code); DBG(("%p %d %s", nc, code, reason)); mg_send_head(nc, code, strlen(reason), "Content-Type: text/plain\r\nConnection: close"); @@ -5127,6 +5140,7 @@ static void mg_http_send_error(struct mg_connection *nc, int code, nc->flags |= MG_F_SEND_AND_CLOSE; } +#if MG_ENABLE_FILESYSTEM static void mg_http_construct_etag(char *buf, size_t buf_len, const cs_stat_t *st) { snprintf(buf, buf_len, "\"%lx.%" INT64_FMT "\"", (unsigned long) st->st_mtime, @@ -5784,6 +5798,7 @@ MG_INTERNAL void mg_find_index_file(const char *path, const char *list, DBG(("[%s] [%s]", path, (*index_file ? *index_file : ""))); } +#if MG_ENABLE_HTTP_URL_REWRITES static int mg_http_send_port_based_redirect( struct mg_connection *c, struct http_message *hm, const struct mg_serve_http_opts *opts) { @@ -5807,6 +5822,103 @@ static int mg_http_send_port_based_redirect( return 0; } +static void mg_reverse_proxy_handler(struct mg_connection *nc, int ev, + void *ev_data) { + struct http_message *hm = (struct http_message *) ev_data; + struct mg_connection *upstream = (struct mg_connection *) nc->user_data; + + switch (ev) { + case MG_EV_CONNECT: + if (*(int *) ev_data != 0) { + mg_http_send_error(upstream, 502, NULL); + } + break; + /* TODO(mkm): handle streaming */ + case MG_EV_HTTP_REPLY: + mg_send(upstream, hm->message.p, hm->message.len); + upstream->flags |= MG_F_SEND_AND_CLOSE; + nc->flags |= MG_F_CLOSE_IMMEDIATELY; + break; + case MG_EV_CLOSE: + upstream->flags |= MG_F_SEND_AND_CLOSE; + break; + } +} + +void mg_handle_reverse_proxy(struct mg_connection *nc, struct http_message *hm, + struct mg_str mount, struct mg_str upstream) { + struct mg_connection *be; + char burl[256], *purl = burl; + char *addr = NULL; + const char *path = NULL; + int i; + struct mg_connect_opts opts; + memset(&opts, 0, sizeof(opts)); + + mg_asprintf(&purl, sizeof(burl), "%.*s%.*s", (int) upstream.len, upstream.p, + (int) (hm->uri.len - mount.len), hm->uri.p + mount.len); + + be = mg_connect_http_base(nc->mgr, mg_reverse_proxy_handler, opts, "http://", + "https://", purl, &path, &addr); + DBG(("Proxying %.*s to %s (rule: %.*s)", (int) hm->uri.len, hm->uri.p, purl, + (int) mount.len, mount.p)); + + if (be == NULL) { + mg_http_send_error(nc, 502, NULL); + goto cleanup; + } + be->user_data = nc; + + /* send request upstream */ + mg_printf(be, "%.*s %s HTTP/1.1\r\n", (int) hm->method.len, hm->method.p, + path); + + mg_printf(be, "Host: %s\r\n", addr); + for (i = 0; i < MG_MAX_HTTP_HEADERS && hm->header_names[i].len > 0; i++) { + struct mg_str hn = hm->header_names[i]; + struct mg_str hv = hm->header_values[i]; + + /* we rewrite the host header */ + if (mg_vcasecmp(&hn, "Host") == 0) continue; + /* + * Don't pass chunked transfer encoding to the client because hm->body is + * already dechunked when we arrive here. + */ + if (mg_vcasecmp(&hn, "Transfer-encoding") == 0 && + mg_vcasecmp(&hv, "chunked") == 0) { + mg_printf(be, "Content-Length: %" SIZE_T_FMT "\r\n", hm->body.len); + continue; + } + mg_printf(be, "%.*s: %.*s\r\n", (int) hn.len, hn.p, (int) hv.len, hv.p); + } + + mg_send(be, "\r\n", 2); + mg_send(be, hm->body.p, hm->body.len); + +cleanup: + if (purl != burl) MG_FREE(purl); +} + +static int mg_http_handle_forwarding(struct mg_connection *nc, + struct http_message *hm, + const struct mg_serve_http_opts *opts) { + const char *rewrites = opts->url_rewrites; + struct mg_str a, b; + struct mg_str p1 = MG_MK_STR("http://"), p2 = MG_MK_STR("https://"); + + while ((rewrites = mg_next_comma_list_entry(rewrites, &a, &b)) != NULL) { + if (mg_strncmp(a, hm->uri, a.len) == 0) { + if (mg_strncmp(b, p1, p1.len) == 0 || mg_strncmp(b, p2, p2.len) == 0) { + mg_handle_reverse_proxy(nc, hm, a, b); + return 1; + } + } + } + + return 0; +} +#endif + MG_INTERNAL int mg_uri_to_local_path(struct http_message *hm, const struct mg_serve_http_opts *opts, char **local_path, @@ -5820,7 +5932,12 @@ MG_INTERNAL int mg_uri_to_local_path(struct http_message *hm, remainder->len = 0; { /* 1. Determine which root to use. */ + +#if MG_ENABLE_HTTP_URL_REWRITES const char *rewrites = opts->url_rewrites; +#else + const char *rewrites = ""; +#endif struct mg_str *hh = mg_get_http_header(hm, "Host"); struct mg_str a, b; /* Check rewrites first. */ @@ -6154,9 +6271,15 @@ void mg_serve_http(struct mg_connection *nc, struct http_message *hm, return; } +#if MG_ENABLE_HTTP_URL_REWRITES + if (mg_http_handle_forwarding(nc, hm, &opts)) { + return; + } + if (mg_http_send_port_based_redirect(nc, hm, &opts)) { return; } +#endif if (opts.document_root == NULL) { opts.document_root = "."; diff --git a/mongoose.h b/mongoose.h index 561c8207..e4cfb936 100644 --- a/mongoose.h +++ b/mongoose.h @@ -1354,6 +1354,7 @@ int mg_vcasecmp(const struct mg_str *str2, const char *str1); struct mg_str mg_strdup(const struct mg_str s); int mg_strcmp(const struct mg_str str1, const struct mg_str str2); +int mg_strncmp(const struct mg_str str1, const struct mg_str str2, size_t n); #ifdef __cplusplus } @@ -1779,6 +1780,11 @@ char *strdup(const char *src); #define MG_ENABLE_MQTT 1 #endif +#ifndef MG_ENABLE_HTTP_URL_REWRITES +#define MG_ENABLE_HTTP_URL_REWRITES \ + (CS_PLATFORM == CS_P_WINDOWS || CS_PLATFORM == CS_P_UNIX) +#endif + #endif /* CS_MONGOOSE_SRC_FEATURES_H_ */ #ifdef MG_MODULE_LINES #line 1 "mongoose/src/net.h" @@ -3226,16 +3232,19 @@ struct mg_serve_http_opts { /* IP ACL. By default, NULL, meaning all IPs are allowed to connect */ const char *ip_acl; +#if MG_ENABLE_HTTP_URL_REWRITES /* URL rewrites. * - * Comma-separated list of `uri_pattern=file_or_directory_path` rewrites. + * Comma-separated list of `uri_pattern=url_file_or_directory_path` rewrites. * When HTTP request is received, Mongoose constructs a file name from the * requested URI by combining `document_root` and the URI. However, if the * rewrite option is used and `uri_pattern` matches requested URI, then - * `document_root` is ignored. Instead, `file_or_directory_path` is used, + * `document_root` is ignored. Instead, `url_file_or_directory_path` is used, * which should be a full path name or a path relative to the web server's - * current working directory. Note that `uri_pattern`, as all Mongoose - * patterns, is a prefix pattern. + * current working directory. It can also be an URI (http:// or https://) + * in which case mongoose will behave as a reverse proxy for that destination. + * + * Note that `uri_pattern`, as all Mongoose patterns, is a prefix pattern. * * If uri_pattern starts with `@` symbol, then Mongoose compares it with the * HOST header of the request. If they are equal, Mongoose sets document root @@ -3249,6 +3258,7 @@ struct mg_serve_http_opts { * automatically appended to the redirect location. */ const char *url_rewrites; +#endif /* DAV document root. If NULL, DAV requests are going to fail. */ const char *dav_document_root; @@ -3449,6 +3459,12 @@ void mg_printf_http_chunk(struct mg_connection *nc, const char *fmt, ...); void mg_send_response_line(struct mg_connection *nc, int status_code, const char *extra_headers); +/* + * Sends an error response. If reason is NULL, the message will be inferred + * from the error code (if supported). + */ +void mg_http_send_error(struct mg_connection *nc, int code, const char *reason); + /* * Sends a redirect response. * `status_code` should be either 301 or 302 and `location` point to the