diff --git a/Dockerfile b/Dockerfile index 487fe7fc..90c71c7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,32 @@ -FROM alpine:latest as build - -ADD . /sslh - -RUN \ - apk add \ - gcc \ - libconfig-dev \ - make \ - musl-dev \ - pcre2-dev \ - perl && \ - cd /sslh && \ - make sslh-select && \ - strip sslh-select - -FROM alpine:latest - -COPY --from=build /sslh/sslh-select /sslh - -RUN apk --no-cache add libconfig pcre2 - -ENTRYPOINT [ "/sslh", "--foreground"] +########################################################################################## +## BUILD APPLICATION +########################################################################################## +FROM alpine:latest as build + +# install required packages first to be able to take advantage of caching +RUN apk add \ + gcc \ + libconfig-dev \ + make \ + musl-dev \ + pcre2-dev \ + perl + +# copy files and build the app +WORKDIR /sslh +ADD . . +RUN make sslh-select && \ + strip sslh-select + + +########################################################################################## +## PACKAGE APPLICATION +########################################################################################## +FROM alpine:latest + +# install required packages first and in parallel with the build container execution +RUN apk --no-cache add libconfig pcre2 + +# copy the final executable from the build container +COPY --from=build /sslh/sslh-select /sslh +ENTRYPOINT [ "/sslh", "--foreground"] diff --git a/Makefile b/Makefile index ec6b46b0..abdf6484 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ AR ?= ar CFLAGS ?=-Wall -O2 -DLIBPCRE -g $(CFLAGS_COV) $(CFLAGS_SAN) LIBS=-lm -lpcre2-8 -OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o tls.o argtable3.o collection.o gap.o tcp-probe.o +OBJS=sslh-conf.o common.o log.o sslh-main.o probe.o http.o tls.o argtable3.o collection.o gap.o tcp-probe.o OBJS_A=libsslh.a FORK_OBJS=sslh-fork.o $(OBJS_A) SELECT_OBJS=processes.o udp-listener.o sslh-select.o hash.o tcp-listener.o $(OBJS_A) @@ -85,7 +85,7 @@ version.h: sslh: sslh-fork sslh-select sslh-ev -$(OBJS) $(FORK_OBJS) $(SELECT_OBJS) $(EV_OBJS): argtable3.h collection.h common.h gap.h hash.h log.h probe.h processes.h sslh-conf.h tcp-listener.h tcp-probe.h tls.h udp-listener.h version.h +$(OBJS) $(FORK_OBJS) $(SELECT_OBJS) $(EV_OBJS): argtable3.h collection.h common.h gap.h hash.h log.h probe.h processes.h sslh-conf.h tcp-listener.h tcp-probe.h http.h tls.h udp-listener.h version.h sslh-conf.c sslh-conf.h: sslhconf.cfg diff --git "a/echo\321\225rv-conf.h" "b/echo\321\225rv-conf.h" index 58701a79..bc3d4640 100644 --- "a/echo\321\225rv-conf.h" +++ "b/echo\321\225rv-conf.h" @@ -55,7 +55,9 @@ struct sslhcfg_protocols_item { int fork; int tfo_ok; int log_level; - int keepalive; + int keepalive; + size_t hostnames_len; + char **hostnames; size_t sni_hostnames_len; char** sni_hostnames; size_t alpn_protocols_len; diff --git a/http.c b/http.c new file mode 100644 index 00000000..682b13f4 --- /dev/null +++ b/http.c @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2011 and 2012, Dustin Lundquist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/* + * This is a minimal HTTP implementation intended only to parse the host name of a + * request + */ +#include +#include /* malloc() */ +#include /* fnmatch() */ +#include "http.h" +#include "sslh-conf.h" +#include "log.h" + +#define HTTP_HEADER_LEN 3 /* GET */ + +typedef struct { + int http_match_hostname : 1; +} HTTP_MATCHMODE; + +struct HTTPProtocol { + HTTP_MATCHMODE match_mode; + int hostname_list_len; + const char** hostname_list; +}; + +static int has_match(const char **, size_t, const char *, size_t); +static int parse_hostname(const struct HTTPProtocol *tls_data, const char *data, size_t data_len); +static int probe_http_method(const char *p, int len, const char *opt); + +/* Parse a HTTP header for request hostname, returning a status code + * + * Returns: + * 0: no match + * 1: match + * < 0: error code (see http.h) + */ +int +parse_http_header(const struct HTTPProtocol *http_data, const char *data, size_t data_len) { + /* Check that our TCP payload is at least large enough for the simplest request */ + if (data_len < HTTP_HEADER_LEN) + return HTTP_ELENGTH; + + /* If it does not have HTTP in the request (HTTP/1.1) then lets check for the method */ + if (memmem(data, data_len, "HTTP", 4) == NULL) { + int res; +#define PROBE_HTTP_METHOD(opt) if ((res = probe_http_method(data, data_len, opt)) != HTTP_NOMATCH) return res + + /* it could be HTTP/1.0 without version: check if it's got an + * HTTP method (RFC2616 5.1.1) */ + PROBE_HTTP_METHOD("OPTIONS"); + PROBE_HTTP_METHOD("GET"); + PROBE_HTTP_METHOD("HEAD"); + PROBE_HTTP_METHOD("POST"); + PROBE_HTTP_METHOD("PUT"); + PROBE_HTTP_METHOD("DELETE"); + PROBE_HTTP_METHOD("TRACE"); + PROBE_HTTP_METHOD("CONNECT"); + +#undef PROBE_HTTP_METHOD + + // if neither match, this isnt a HTTP request + return HTTP_NOMATCH; + } + + /* By now we know it's HTTP. if hostname is set, parse request to see if + * they match. Otherwise, it's a match already */ + if (http_data && http_data->match_mode.http_match_hostname) { + return parse_hostname(http_data, data, data_len); + } else { + return HTTP_MATCH; + } +} + +static int +parse_hostname(const struct HTTPProtocol *http_data, const char *data, size_t data_len) +{ + // see if already have the hostname + const char *start = memmem(data, data_len, "Host: ", 6); + if (start != NULL) + { + // move the pointer to the end of the string + start += 6; + + // calculate the number of charecters up to this point + size_t len = start - data; + + // searching for the end of this line + const char *end = memchr(start, '\r', data_len - len); + + // if we have the end, we are ready to parse it + if (end != NULL) + { + // calculate host name length + len = end - start; + + return has_match(http_data->hostname_list, http_data->hostname_list_len, start, len); + } + } + else if ( + // or if we have already reached the end of the request + memmem(data, data_len, "\r\n\r\n", 4) + ) + { + // no host informaiton available for this request + return HTTP_ENOHOST; + } + + return HTTP_ELENGTH; +} + +static int +probe_http_method(const char *p, int len, const char *opt) +{ + if (len < strlen(opt)) + return HTTP_ELENGTH; + + return !strncmp(p, opt, strlen(opt)); +} + +static int +has_match(const char** list, size_t list_len, const char* name, size_t name_len) { + const char **item; + int i; + char *name_nullterminated = malloc(name_len+1); + CHECK_ALLOC(name_nullterminated, "malloc"); + memcpy(name_nullterminated, name, name_len); + name_nullterminated[name_len]='\0'; + + for (i = 0; i < list_len; i++) { + item = &list[i]; + print_message(msg_probe_error, "matching [%.*s] with [%s]\n", (int)name_len, name, *item); + if(!fnmatch(*item, name_nullterminated, 0)) { + free(name_nullterminated); + return 1; + } + } + free(name_nullterminated); + return 0; +} + +struct HTTPProtocol * +new_http_data() { + struct HTTPProtocol *http_data = malloc(sizeof(struct HTTPProtocol)); + CHECK_ALLOC(http_data, "malloc"); + + memset(http_data, 0, sizeof(*http_data)); + + return http_data; +} + +struct HTTPProtocol * +http_data_set_list(struct HTTPProtocol *http_data, const char** list, size_t list_len) { + http_data->hostname_list = list; + http_data->hostname_list_len = list_len; + http_data->match_mode.http_match_hostname = 1; + return http_data; +} diff --git a/http.h b/http.h new file mode 100644 index 00000000..a646092b --- /dev/null +++ b/http.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2011 and 2012, Dustin Lundquist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +#ifndef HTTP_H +#define HTTP_H + +#include "common.h" + +struct HTTPProtocol; + +int parse_http_header(const struct HTTPProtocol *http_data, const char *data, size_t data_len); + +struct HTTPProtocol *new_http_data(); +struct HTTPProtocol *http_data_set_list(struct HTTPProtocol *, const char **, size_t); + +#define HTTP_MATCH 1 +#define HTTP_NOMATCH 0 +#define HTTP_ELENGTH -1 /* Incomplete request */ +#define HTTP_ENOHOST -2 /* No host information found */ + +#endif diff --git a/probe.c b/probe.c index aaacb9c2..9dab60c9 100644 --- a/probe.c +++ b/probe.c @@ -235,38 +235,15 @@ static int is_xmpp_protocol( const char *p, ssize_t len, struct sslhcfg_protocol return PROBE_NEXT; } -static int probe_http_method(const char *p, int len, const char *opt) -{ - if (len < strlen(opt)) - return PROBE_AGAIN; - - return !strncmp(p, opt, strlen(opt)); -} - -/* Is the buffer the beginning of an HTTP connection? */ +/* Says if it's HTTP, optionally with hostname list in proto->data */ static int is_http_protocol(const char *p, ssize_t len, struct sslhcfg_protocols_item* proto) -{ - int res; - /* If it's got HTTP in the request (HTTP/1.1) then it's HTTP */ - if (memmem(p, len, "HTTP", 4)) - return PROBE_MATCH; - -#define PROBE_HTTP_METHOD(opt) if ((res = probe_http_method(p, len, opt)) != PROBE_NEXT) return res - - /* Otherwise it could be HTTP/1.0 without version: check if it's got an - * HTTP method (RFC2616 5.1.1) */ - PROBE_HTTP_METHOD("OPTIONS"); - PROBE_HTTP_METHOD("GET"); - PROBE_HTTP_METHOD("HEAD"); - PROBE_HTTP_METHOD("POST"); - PROBE_HTTP_METHOD("PUT"); - PROBE_HTTP_METHOD("DELETE"); - PROBE_HTTP_METHOD("TRACE"); - PROBE_HTTP_METHOD("CONNECT"); - -#undef PROBE_HTTP_METHOD - - return PROBE_NEXT; +{ + switch (parse_http_header(proto->data, p, len)) { + case HTTP_MATCH: return PROBE_MATCH; + case HTTP_NOMATCH: return PROBE_NEXT; + case HTTP_ELENGTH: return PROBE_AGAIN; + default: return PROBE_NEXT; + } } /* Says if it's TLS, optionally with SNI and ALPN lists in proto->data */ diff --git a/probe.h b/probe.h index d0b768ba..d4920624 100644 --- a/probe.h +++ b/probe.h @@ -5,6 +5,7 @@ #include "common.h" #include "tls.h" +#include "http.h" #include "log.h" typedef enum { diff --git a/sslh-conf.c b/sslh-conf.c index b60cfc1e..48242e96 100644 --- a/sslh-conf.c +++ b/sslh-conf.c @@ -679,6 +679,22 @@ static struct config_desc table_sslhcfg_protocols[] = { /* default_val*/ .default_val.def_bool = 0 }, + { + /* name */ "hostnames", + /* type */ CFG_ARRAY, + /* sub_group*/ NULL, + /* arg_cl */ NULL, + /* base_addr */ NULL, + /* offset */ offsetof(struct sslhcfg_protocols_item, hostnames), + /* offset_len */ offsetof(struct sslhcfg_protocols_item, hostnames_len), + /* offset_present */ 0, + /* size */ sizeof(char*), + /* array_type */ CFG_STRING, + /* mandatory */ 1, + /* optional */ 0, + /* default_val*/ .default_val.def_int = 0 + }, + { /* name */ "sni_hostnames", /* type */ CFG_ARRAY, @@ -2306,6 +2322,12 @@ static void sslhcfg_protocols_fprint( fprintf(out, "keepalive: %d", sslhcfg_protocols->keepalive); fprintf(out, "\n"); indent(out, depth); + fprintf(out, "hostnames [%zu]:\n", sslhcfg_protocols->hostnames_len); + for (i = 0; i < sslhcfg_protocols->hostnames_len; i++) { + indent(out, depth+1); + fprintf(out, "%d:\t%s\n", i, sslhcfg_protocols->hostnames[i]); + } + indent(out, depth); fprintf(out, "sni_hostnames [%zu]:\n", sslhcfg_protocols->sni_hostnames_len); for (i = 0; i < sslhcfg_protocols->sni_hostnames_len; i++) { indent(out, depth+1); diff --git a/sslh-conf.h b/sslh-conf.h index e52dfe02..c7c06f62 100644 --- a/sslh-conf.h +++ b/sslh-conf.h @@ -60,7 +60,9 @@ struct sslhcfg_protocols_item { int transparent; int resolve_on_forward; int log_level; - int keepalive; + int keepalive; + size_t hostnames_len; + char **hostnames; size_t sni_hostnames_len; char** sni_hostnames; size_t alpn_protocols_len; diff --git a/sslh-main.c b/sslh-main.c index b5ead6d6..aaeb1404 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -149,6 +149,14 @@ static void config_protocols() setup_regex_probe(&cfg.protocols[i]); } + if (!strcmp(cfg.protocols[i].name, "http")) { + cfg.protocols[i].data = (void*)new_http_data(); + if (cfg.protocols[i].hostnames_len) + http_data_set_list(cfg.protocols[i].data, + (const char**) cfg.protocols[i].hostnames, + cfg.protocols[i].hostnames_len); + } + if (!strcmp(cfg.protocols[i].name, "tls")) { cfg.protocols[i].data = (void*)new_tls_data(); if (cfg.protocols[i].sni_hostnames_len) @@ -180,6 +188,15 @@ void config_sanity_check(struct sslhcfg_item* cfg) #endif for (i = 0; i < cfg->protocols_len; ++i) { + if (strcmp(cfg->protocols[i].name, "http")) { + if (cfg->protocols[i].hostnames_len) { + print_message(msg_config_error, "name: \"%s\"; host: \"%s\"; port: \"%s\": " + "Config option hostnames is only applicable for http\n", + cfg->protocols[i].name, cfg->protocols[i].host, cfg->protocols[i].port); + exit(1); + } + } + if (strcmp(cfg->protocols[i].name, "tls")) { if (cfg->protocols[i].sni_hostnames_len) { print_message(msg_config_error, "name: \"%s\"; host: \"%s\"; port: \"%s\": "