diff options
39 files changed, 3233 insertions, 184 deletions
@@ -23,3 +23,33 @@ httpd is free software under OpenBSD's ISC-style license. > WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN > ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF > OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +* One exception is +[`patterns.c`](https://github.com/reyk/httpd/blob/master/httpd/patterns.c) +that is derived from the pattern matching code in Lua's `lstrlib.c`. + +> Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> +> Copyright (C) 1994-2015 Lua.org, PUC-Rio. +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +> +> Derived from Lua 5.3.1: +> $Id: patterns.c,v 1.2 2015/06/23 15:35:20 semarie Exp $ +> Standard library for string operations and pattern-matching diff --git a/httpd/Makefile b/httpd/Makefile index 885ad42..e01dec1 100644 --- a/httpd/Makefile +++ b/httpd/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.27 2015/02/23 10:39:10 reyk Exp $ +# $OpenBSD: Makefile,v 1.28 2015/06/23 15:23:14 reyk Exp $ PROG= httpd SRCS= parse.y @@ -6,6 +6,9 @@ SRCS+= config.c control.c httpd.c log.c logger.c proc.c SRCS+= server.c server_http.c server_file.c server_fcgi.c MAN= httpd.8 httpd.conf.5 +SRCS+= patterns.c +MAN+= patterns.7 + LDADD= -levent -ltls -lssl -lcrypto -lutil DPADD= ${LIBEVENT} ${LIBTLS} ${LIBSSL} ${LIBCRYPTO} ${LIBUTIL} #DEBUG= -g -DDEBUG=3 -O0 diff --git a/httpd/config.c b/httpd/config.c index 7635c33..3c88990 100644 --- a/httpd/config.c +++ b/httpd/config.c @@ -1,4 +1,4 @@ -/* $OpenBSD: config.c,v 1.37 2015/04/11 14:52:49 jsing Exp $ */ +/* $OpenBSD: config.c,v 1.39 2015/07/15 16:00:39 jsing Exp $ */ /* * Copyright (c) 2011 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -193,14 +193,6 @@ config_setserver(struct httpd *env, struct server *srv) iov[c].iov_base = srv->srv_conf.return_uri; iov[c++].iov_len = srv->srv_conf.return_uri_len; } - if (srv->srv_conf.tls_cert_len != 0) { - iov[c].iov_base = srv->srv_conf.tls_cert; - iov[c++].iov_len = srv->srv_conf.tls_cert_len; - } - if (srv->srv_conf.tls_key_len != 0) { - iov[c].iov_base = srv->srv_conf.tls_key; - iov[c++].iov_len = srv->srv_conf.tls_key_len; - } if (id == PROC_SERVER && (srv->srv_conf.flags & SRVFLAG_LOCATION) == 0) { @@ -220,6 +212,9 @@ config_setserver(struct httpd *env, struct server *srv) return (-1); } } + + /* Configure TLS if necessary. */ + config_settls(env, srv); } else { if (proc_composev_imsg(ps, id, -1, IMSG_CFG_SERVER, -1, iov, c) != 0) { @@ -235,6 +230,72 @@ config_setserver(struct httpd *env, struct server *srv) } int +config_settls(struct httpd *env, struct server *srv) +{ + struct privsep *ps = env->sc_ps; + struct tls_config tls; + struct iovec iov[2]; + size_t c; + + if ((srv->srv_conf.flags & SRVFLAG_TLS) == 0) + return (0); + + log_debug("%s: configuring TLS for %s", __func__, srv->srv_conf.name); + + if (srv->srv_conf.tls_cert_len != 0) { + DPRINTF("%s: sending TLS cert \"%s[%u]\" to %s fd %d", __func__, + srv->srv_conf.name, srv->srv_conf.id, + ps->ps_title[PROC_SERVER], srv->srv_s); + + memset(&tls, 0, sizeof(tls)); + tls.id = srv->srv_conf.id; + tls.port = srv->srv_conf.port; + memcpy(&tls.ss, &srv->srv_conf.ss, sizeof(tls.ss)); + tls.tls_cert_len = srv->srv_conf.tls_cert_len; + + c = 0; + iov[c].iov_base = &tls; + iov[c++].iov_len = sizeof(tls); + iov[c].iov_base = srv->srv_conf.tls_cert; + iov[c++].iov_len = srv->srv_conf.tls_cert_len; + + if (proc_composev_imsg(ps, PROC_SERVER, -1, IMSG_CFG_TLS, -1, + iov, c) != 0) { + log_warn("%s: failed to compose IMSG_CFG_TLS imsg for " + "`%s'", __func__, srv->srv_conf.name); + return (-1); + } + } + + if (srv->srv_conf.tls_key_len != 0) { + DPRINTF("%s: sending TLS key \"%s[%u]\" to %s fd %d", __func__, + srv->srv_conf.name, srv->srv_conf.id, + ps->ps_title[PROC_SERVER], srv->srv_s); + + memset(&tls, 0, sizeof(tls)); + tls.id = srv->srv_conf.id; + tls.port = srv->srv_conf.port; + memcpy(&tls.ss, &srv->srv_conf.ss, sizeof(tls.ss)); + tls.tls_key_len = srv->srv_conf.tls_key_len; + + c = 0; + iov[c].iov_base = &tls; + iov[c++].iov_len = sizeof(tls); + iov[c].iov_base = srv->srv_conf.tls_key; + iov[c++].iov_len = srv->srv_conf.tls_key_len; + + if (proc_composev_imsg(ps, PROC_SERVER, -1, IMSG_CFG_TLS, -1, + iov, c) != 0) { + log_warn("%s: failed to compose IMSG_CFG_TLS imsg for " + "`%s'", __func__, srv->srv_conf.name); + return (-1); + } + } + + return (0); +} + +int config_getserver_auth(struct httpd *env, struct server_config *srv_conf) { struct privsep *ps = env->sc_ps; @@ -422,9 +483,7 @@ config_getserver(struct httpd *env, struct imsg *imsg) /* Reset these variables to avoid free'ing invalid pointers */ serverconfig_reset(&srv_conf); - if ((IMSG_DATA_SIZE(imsg) - s) < - (srv_conf.tls_cert_len + srv_conf.tls_key_len + - srv_conf.return_uri_len)) { + if ((IMSG_DATA_SIZE(imsg) - s) < (size_t)srv_conf.return_uri_len) { log_debug("%s: invalid message length", __func__); goto fail; } @@ -475,30 +534,69 @@ config_getserver(struct httpd *env, struct imsg *imsg) goto fail; s += srv->srv_conf.return_uri_len; } - if (srv->srv_conf.tls_cert_len != 0) { + + return (0); + + fail: + if (imsg->fd != -1) + close(imsg->fd); + if (srv != NULL) + serverconfig_free(&srv->srv_conf); + free(srv); + + return (-1); +} + +int +config_gettls(struct httpd *env, struct imsg *imsg) +{ +#ifdef DEBUG + struct privsep *ps = env->sc_ps; +#endif + struct server *srv = NULL; + struct tls_config tls_conf; + u_int8_t *p = imsg->data; + size_t s; + + IMSG_SIZE_CHECK(imsg, &tls_conf); + memcpy(&tls_conf, p, sizeof(tls_conf)); + s = sizeof(tls_conf); + + if ((IMSG_DATA_SIZE(imsg) - s) < + (tls_conf.tls_cert_len + tls_conf.tls_key_len)) { + log_debug("%s: invalid message length", __func__); + goto fail; + } + + /* Find server with matching listening socket. */ + if ((srv = server_byaddr((struct sockaddr *) + &tls_conf.ss, tls_conf.port)) == NULL) { + log_debug("%s: server not found", __func__); + goto fail; + } + + DPRINTF("%s: %s %d TLS configuration \"%s[%u]\"", __func__, + ps->ps_title[privsep_process], ps->ps_instance, + srv->srv_conf.name, srv->srv_conf.id); + + if (tls_conf.tls_cert_len != 0) { + srv->srv_conf.tls_cert_len = tls_conf.tls_cert_len; if ((srv->srv_conf.tls_cert = get_data(p + s, - srv->srv_conf.tls_cert_len)) == NULL) + tls_conf.tls_cert_len)) == NULL) goto fail; - s += srv->srv_conf.tls_cert_len; + s += tls_conf.tls_cert_len; } - if (srv->srv_conf.tls_key_len != 0) { + if (tls_conf.tls_key_len != 0) { + srv->srv_conf.tls_key_len = tls_conf.tls_key_len; if ((srv->srv_conf.tls_key = get_data(p + s, - srv->srv_conf.tls_key_len)) == NULL) + tls_conf.tls_key_len)) == NULL) goto fail; - s += srv->srv_conf.tls_key_len; + s += tls_conf.tls_key_len; } return (0); fail: - if (imsg->fd != -1) - close(imsg->fd); - if (srv != NULL) { - free(srv->srv_conf.tls_cert); - free(srv->srv_conf.tls_key); - } - free(srv); - return (-1); } diff --git a/httpd/control.c b/httpd/control.c index fe60375..5c71545 100644 --- a/httpd/control.c +++ b/httpd/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.6 2015/01/21 22:21:05 reyk Exp $ */ +/* $OpenBSD: control.c,v 1.7 2015/05/28 17:08:08 florian Exp $ */ /* * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> @@ -126,7 +126,6 @@ control_cleanup(struct control_sock *cs) return; event_del(&cs->cs_ev); event_del(&cs->cs_evt); - (void)unlink(cs->cs_name); } /* ARGSUSED */ diff --git a/httpd/http.h b/httpd/http.h index 00cf235..9042342 100644 --- a/httpd/http.h +++ b/httpd/http.h @@ -1,4 +1,4 @@ -/* $OpenBSD: http.h,v 1.12 2015/02/11 12:52:01 florian Exp $ */ +/* $OpenBSD: http.h,v 1.13 2015/06/11 18:49:09 reyk Exp $ */ /* * Copyright (c) 2012 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -16,8 +16,8 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#ifndef _HTTP_H -#define _HTTP_H +#ifndef HTTP_H +#define HTTP_H #define HTTP_PORT 80 #define HTTPS_PORT 443 @@ -250,4 +250,4 @@ struct http_descriptor { struct kvtree http_headers; }; -#endif /* _HTTP_H */ +#endif /* HTTP_H */ diff --git a/httpd/httpd.c b/httpd/httpd.c index c6f183f..5f6788c 100644 --- a/httpd/httpd.c +++ b/httpd/httpd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: httpd.c,v 1.35 2015/02/23 18:43:18 reyk Exp $ */ +/* $OpenBSD: httpd.c,v 1.37 2015/06/03 02:24:36 millert Exp $ */ /* * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org> @@ -80,6 +80,8 @@ parent_sig_handler(int sig, short event, void *arg) /* FALLTHROUGH */ case SIGCHLD: do { + int len; + pid = waitpid(WAIT_ANY, &status, WNOHANG); if (pid <= 0) continue; @@ -87,17 +89,21 @@ parent_sig_handler(int sig, short event, void *arg) fail = 0; if (WIFSIGNALED(status)) { fail = 1; - asprintf(&cause, "terminated; signal %d", + len = asprintf(&cause, "terminated; signal %d", WTERMSIG(status)); } else if (WIFEXITED(status)) { if (WEXITSTATUS(status) != 0) { fail = 1; - asprintf(&cause, "exited abnormally"); + len = asprintf(&cause, + "exited abnormally"); } else - asprintf(&cause, "exited okay"); + len = asprintf(&cause, "exited okay"); } else fatalx("unexpected cause of SIGCHLD"); + if (len == -1) + fatal("asprintf"); + die = 1; for (id = 0; id < PROC_MAX; id++) @@ -210,7 +216,7 @@ main(int argc, char *argv[]) errx(1, "unknown user %s", HTTPD_USER); /* Configure the control socket */ - ps->ps_csock.cs_name = HTTPD_SOCKET; + ps->ps_csock.cs_name = NULL; log_init(debug); log_verbose(verbose); @@ -404,6 +410,8 @@ parent_shutdown(struct httpd *env) proc_kill(env->sc_ps); control_cleanup(&env->sc_ps->ps_csock); + if (env->sc_ps->ps_csock.cs_name != NULL) + (void)unlink(env->sc_ps->ps_csock.cs_name); free(env->sc_ps); free(env); diff --git a/httpd/httpd.conf.5 b/httpd/httpd.conf.5 index 26d6798..b3eaad8 100644 --- a/httpd/httpd.conf.5 +++ b/httpd/httpd.conf.5 @@ -1,4 +1,4 @@ -.\" $OpenBSD: httpd.conf.5,v 1.57 2015/03/26 19:16:57 jmc Exp $ +.\" $OpenBSD: httpd.conf.5,v 1.64 2015/07/15 17:10:47 jsing Exp $ .\" .\" Copyright (c) 2014, 2015 Reyk Floeter <reyk@openbsd.org> .\" @@ -14,7 +14,7 @@ .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: March 26 2015 $ +.Dd $Mdocdate: July 15 2015 $ .Dt HTTPD.CONF 5 .Os .Sh NAME @@ -131,14 +131,38 @@ The configured web servers. .Pp Each .Ic server -must have a -.Ar name -and include one or more lines of the following syntax: +section starts with a declaration of the server +.Ar name : +.Bl -tag -width Ds +.It Ic server Ar name Brq ... +Match the server name using shell globbing rules. +This can be an explicit name, +.Ar www.example.com , +or a name including wildcards, +.Ar *.example.com . +.It Ic server match Ar name Brq ... +Match the server name using pattern matching, +see +.Xr patterns 7 . +.El +.Pp +Followed by a block of options that is enclosed in curly brackets: .Bl -tag -width Ds .It Ic alias Ar name Specify an additional alias .Ar name for this server. +.It Ic alias match Ar name +Like the +.Ic alias +option, +but +.Ic match +the +.Ar name +using pattern matching instead of shell globbing rules, +see +.Xr patterns 7 . .It Oo Ic no Oc Ic authenticate Oo Ar realm Oc Ic with Pa htpasswd Authenticate a remote user for .Ar realm @@ -152,7 +176,7 @@ Use the directive to disable authentication in a location. .It Ic block drop Drop the connection without sending an error page. -.It Ic block Op Ic return Ar code Op uri +.It Ic block Op Ic return Ar code Op Ar uri Close the connection and send an error page. If the optional return code is not specified, .Xr httpd 8 @@ -166,7 +190,7 @@ argument can be used with return codes in the 3xx range to send a header for redirection to a specified URI. .Pp The -.Ar url +.Ar uri may contain predefined macros that will be expanded at runtime: .Pp .Bl -tag -width $DOCUMENT_URI -offset indent -compact @@ -188,6 +212,12 @@ The configured IP address of the server. The configured TCP server port of the server. .It Ic $SERVER_NAME The name of the server. +.It Pf % Ar n +The capture index +.Ar n +of a string that was captured by the enclosing +.Ic location match +option. .El .It Ic connection Ar option Set the specified options and limits for HTTP connections. @@ -247,6 +277,22 @@ except .Ic location and .Ic tcp . +.It Ic location match Ar path Brq ... +Like the +.Ic location +option, +but +.Ic match +the +.Ar path +using pattern matching instead of shell globbing rules, +see +.Xr patterns 7 . +The pattern may contain captures that can be used in the +.Ar uri +of an enclosed +.Ic block return +option. .It Oo Ic no Oc Ic log Op Ar option Set the specified logging options. Logging is enabled by default using the standard @@ -366,6 +412,8 @@ Specify the certificate to use for this server. The .Ar file should contain a PEM encoded certificate. +The default is +.Pa /etc/ssl/server.crt . .It Ic ciphers Ar string Specify the TLS cipher string. If not specified, the default value @@ -392,6 +440,8 @@ should contain a PEM encoded private key and reside outside of the .Xr chroot 2 root directory of .Nm httpd . +The default is +.Pa /etc/ssl/private/server.key . .It Ic protocols Ar string Specify the TLS protocols to enable for this server. If not specified, the value @@ -451,14 +501,14 @@ server "default" { } types { - text/css css - text/html htm html - text/txt txt - image/gif gif - image/jpeg jpg jpeg - image/png png - application/javascript js - application/xml xml + text/css css + text/html html htm + text/txt txt + image/gif gif + image/jpeg jpeg jpg + image/png png + application/javascript js + application/xml xml } .Ed .Pp @@ -514,16 +564,9 @@ server "www.example.com" { listen on 10.0.0.1 port 80 } .Ed -.Pp -The syntax of the types section is also compatible with the format -used by nginx, so it is possible to include its -.Pa mime.types -file directly: -.Bd -literal -offset indent -include "/etc/nginx/mime.types" -.Ed .Sh SEE ALSO .Xr htpasswd 1 , +.Xr patterns 7 , .Xr httpd 8 , .Xr slowcgi 8 .Sh AUTHORS diff --git a/httpd/httpd.h b/httpd/httpd.h index 50f300a..2cb7934 100644 --- a/httpd/httpd.h +++ b/httpd/httpd.h @@ -1,4 +1,4 @@ -/* $OpenBSD: httpd.h,v 1.82 2015/03/15 22:08:45 florian Exp $ */ +/* $OpenBSD: httpd.h,v 1.88 2015/07/16 16:29:25 florian Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -34,6 +34,9 @@ #include <event.h> #include <imsg.h> #include <tls.h> +#include <vis.h> + +#include "patterns.h" #define CONF_FILE "/etc/httpd.conf" #define HTTPD_SOCKET "/var/run/httpd.sock" @@ -45,6 +48,7 @@ #define HTTPD_LOGROOT "/logs" #define HTTPD_ACCESS_LOG "access.log" #define HTTPD_ERROR_LOG "error.log" +#define HTTPD_LOGVIS VIS_NL|VIS_TAB|VIS_CSTYLE #define HTTPD_TLS_CERT "/etc/ssl/server.crt" #define HTTPD_TLS_KEY "/etc/ssl/private/server.key" #define HTTPD_TLS_CIPHERS "HIGH:!aNULL" @@ -62,6 +66,8 @@ #define SERVER_MAXREQUESTBODY 1048576 /* 1M */ #define SERVER_BACKLOG 10 #define SERVER_OUTOF_FD_RETRIES 5 +#define SERVER_MAX_PREFETCH 256 +#define SERVER_MIN_PREFETCHED 32 #define MEDIATYPE_NAMEMAX 128 /* file name extension */ #define MEDIATYPE_TYPEMAX 64 /* length of type/subtype */ @@ -199,6 +205,7 @@ enum imsg_type { IMSG_CTL_START, IMSG_CTL_REOPEN, IMSG_CFG_SERVER, + IMSG_CFG_TLS, IMSG_CFG_MEDIA, IMSG_CFG_AUTH, IMSG_CFG_DONE, @@ -278,6 +285,7 @@ struct client { void *clt_srv_conf; u_int32_t clt_srv_id; struct sockaddr_storage clt_srv_ss; + struct str_match clt_srv_match; int clt_s; in_port_t clt_port; @@ -341,12 +349,15 @@ SPLAY_HEAD(client_tree, client); #define SRVFLAG_NO_AUTH 0x00020000 #define SRVFLAG_BLOCK 0x00040000 #define SRVFLAG_NO_BLOCK 0x00080000 +#define SRVFLAG_LOCATION_MATCH 0x00100000 +#define SRVFLAG_SERVER_MATCH 0x00200000 #define SRVFLAG_BITS \ "\10\01INDEX\02NO_INDEX\03AUTO_INDEX\04NO_AUTO_INDEX" \ "\05ROOT\06LOCATION\07FCGI\10NO_FCGI\11LOG\12NO_LOG\13SOCKET" \ "\14SYSLOG\15NO_SYSLOG\16TLS\17ACCESS_LOG\20ERROR_LOG" \ - "\21AUTH\22NO_AUTH\23BLOCK\24NO_BLOCK" + "\21AUTH\22NO_AUTH\23BLOCK\24NO_BLOCK\25LOCATION_MATCH" \ + "\26SERVER_MATCH" #define TCPFLAG_NODELAY 0x01 #define TCPFLAG_NNODELAY 0x02 @@ -436,6 +447,16 @@ struct server_config { }; TAILQ_HEAD(serverhosts, server_config); +struct tls_config { + u_int32_t id; + + in_port_t port; + struct sockaddr_storage ss; + + size_t tls_cert_len; + size_t tls_key_len; +}; + struct server { TAILQ_ENTRY(server) srv_entry; struct server_config srv_conf; @@ -557,7 +578,7 @@ int server_headers(struct client *, void *, int (*)(struct client *, struct kv *, void *), void *); int server_writeresponse_http(struct client *); int server_response_http(struct client *, u_int, struct media_type *, - size_t, time_t); + off_t, time_t); void server_reset_http(struct client *); void server_close_http(struct client *); int server_response(struct httpd *, struct client *); @@ -674,7 +695,9 @@ int config_setreset(struct httpd *, u_int); int config_getreset(struct httpd *, struct imsg *); int config_getcfg(struct httpd *, struct imsg *); int config_setserver(struct httpd *, struct server *); +int config_settls(struct httpd *, struct server *); int config_getserver(struct httpd *, struct imsg *); +int config_gettls(struct httpd *, struct imsg *); int config_setmedia(struct httpd *, struct media_type *); int config_getmedia(struct httpd *, struct imsg *); int config_setauth(struct httpd *, struct auth *); diff --git a/httpd/parse.y b/httpd/parse.y index 0aae421..0870819 100644 --- a/httpd/parse.y +++ b/httpd/parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: parse.y,v 1.67 2015/04/01 04:51:15 jsg Exp $ */ +/* $OpenBSD: parse.y,v 1.70 2015/07/16 19:05:28 reyk Exp $ */ /* * Copyright (c) 2007 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -107,7 +107,7 @@ int host_if(const char *, struct addresslist *, int host(const char *, struct addresslist *, int, struct portrange *, const char *, int); void host_free(struct addresslist *); -struct server *server_inherit(struct server *, const char *, +struct server *server_inherit(struct server *, struct server_config *, struct server_config *); int getservice(char *); int is_if_in_group(const char *, const char *); @@ -131,14 +131,14 @@ typedef struct { %token ACCESS ALIAS AUTO BACKLOG BODY BUFFER CERTIFICATE CHROOT CIPHERS COMMON %token COMBINED CONNECTION DHE DIRECTORY ECDHE ERR FCGI INDEX IP KEY LISTEN -%token LOCATION LOG LOGDIR MAXIMUM NO NODELAY ON PORT PREFORK PROTOCOLS +%token LOCATION LOG LOGDIR MATCH MAXIMUM NO NODELAY ON PORT PREFORK PROTOCOLS %token REQUEST REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TIMEOUT %token TLS TYPES %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS %token <v.string> STRING %token <v.number> NUMBER %type <v.port> port -%type <v.number> opttls +%type <v.number> opttls optmatch %type <v.tv> timeout %type <v.string> numberstring optstring %type <v.auth> authopts @@ -200,26 +200,26 @@ main : PREFORK NUMBER { } ; -server : SERVER STRING { +server : SERVER optmatch STRING { struct server *s; if (!loadcfg) { - free($2); + free($3); YYACCEPT; } if ((s = calloc(1, sizeof (*s))) == NULL) fatal("out of memory"); - if (strlcpy(s->srv_conf.name, $2, + if (strlcpy(s->srv_conf.name, $3, sizeof(s->srv_conf.name)) >= sizeof(s->srv_conf.name)) { yyerror("server name truncated"); - free($2); + free($3); free(s); YYERROR; } - free($2); + free($3); strlcpy(s->srv_conf.root, HTTPD_DOCROOT, sizeof(s->srv_conf.root)); @@ -235,7 +235,9 @@ server : SERVER STRING { s->srv_conf.timeout.tv_sec = SERVER_TIMEOUT; s->srv_conf.maxrequests = SERVER_MAXREQUESTS; s->srv_conf.maxrequestbody = SERVER_MAXREQUESTBODY; - s->srv_conf.flags |= SRVFLAG_LOG; + s->srv_conf.flags = SRVFLAG_LOG; + if ($2) + s->srv_conf.flags |= SRVFLAG_SERVER_MATCH; s->srv_conf.logformat = LOG_FORMAT_COMMON; s->srv_conf.tls_protocols = TLS_PROTOCOLS_DEFAULT; if ((s->srv_conf.tls_cert_file = @@ -334,7 +336,7 @@ server : SERVER STRING { continue; if ((sn = server_inherit(srv, - b->name, a)) == NULL) { + b, a)) == NULL) { serverconfig_free(srv_conf); free(srv); YYABORT; @@ -405,30 +407,37 @@ serveroptsl : LISTEN ON STRING opttls port { } if (alias != NULL) { + /* IP-based; use name match flags from parent */ + alias->flags &= ~SRVFLAG_SERVER_MATCH; + alias->flags |= srv->srv_conf.flags & + SRVFLAG_SERVER_MATCH; TAILQ_INSERT_TAIL(&srv->srv_hosts, alias, entry); } } - | ALIAS STRING { + | ALIAS optmatch STRING { struct server_config *alias; if (parentsrv != NULL) { yyerror("alias inside location"); - free($2); + free($3); YYERROR; } if ((alias = calloc(1, sizeof(*alias))) == NULL) fatal("out of memory"); - if (strlcpy(alias->name, $2, sizeof(alias->name)) >= + if (strlcpy(alias->name, $3, sizeof(alias->name)) >= sizeof(alias->name)) { yyerror("server alias truncated"); - free($2); + free($3); free(alias); YYERROR; } - free($2); + free($3); + + if ($2) + alias->flags |= SRVFLAG_SERVER_MATCH; TAILQ_INSERT_TAIL(&srv->srv_hosts, alias, entry); } @@ -456,38 +465,38 @@ serveroptsl : LISTEN ON STRING opttls port { | fastcgi | authenticate | filter - | LOCATION STRING { + | LOCATION optmatch STRING { struct server *s; if (srv->srv_conf.ss.ss_family == AF_UNSPEC) { yyerror("listen address not specified"); - free($2); + free($3); YYERROR; } if (parentsrv != NULL) { - yyerror("location %s inside location", $2); - free($2); + yyerror("location %s inside location", $3); + free($3); YYERROR; } if (!loadcfg) { - free($2); + free($3); YYACCEPT; } if ((s = calloc(1, sizeof (*s))) == NULL) fatal("out of memory"); - if (strlcpy(s->srv_conf.location, $2, + if (strlcpy(s->srv_conf.location, $3, sizeof(s->srv_conf.location)) >= sizeof(s->srv_conf.location)) { yyerror("server location truncated"); - free($2); + free($3); free(s); YYERROR; } - free($2); + free($3); if (strlcpy(s->srv_conf.name, srv->srv_conf.name, sizeof(s->srv_conf.name)) >= @@ -501,6 +510,8 @@ serveroptsl : LISTEN ON STRING opttls port { /* A location entry uses the parent id */ s->srv_conf.parent_id = srv->srv_conf.id; s->srv_conf.flags = SRVFLAG_LOCATION; + if ($2) + s->srv_conf.flags |= SRVFLAG_LOCATION_MATCH; s->srv_s = -1; memcpy(&s->srv_conf.ss, &srv->srv_conf.ss, sizeof(s->srv_conf.ss)); @@ -884,6 +895,10 @@ block : BLOCK { } ; +optmatch : /* empty */ { $$ = 0; } + | MATCH { $$ = 1; } + ; + optstring : /* empty */ { $$ = NULL; } | STRING { $$ = $1; } ; @@ -1108,6 +1123,7 @@ lookup(char *s) { "location", LOCATION }, { "log", LOG }, { "logdir", LOGDIR }, + { "match", MATCH }, { "max", MAXIMUM }, { "no", NO }, { "nodelay", NODELAY }, @@ -1889,7 +1905,7 @@ host_free(struct addresslist *al) } struct server * -server_inherit(struct server *src, const char *name, +server_inherit(struct server *src, struct server_config *alias, struct server_config *addr) { struct server *dst, *s, *dstl; @@ -1927,7 +1943,7 @@ server_inherit(struct server *src, const char *name, } /* Now set alias and listen address */ - strlcpy(dst->srv_conf.name, name, sizeof(dst->srv_conf.name)); + strlcpy(dst->srv_conf.name, alias->name, sizeof(dst->srv_conf.name)); memcpy(&dst->srv_conf.ss, &addr->ss, sizeof(dst->srv_conf.ss)); dst->srv_conf.port = addr->port; dst->srv_conf.prefixlen = addr->prefixlen; @@ -1936,6 +1952,10 @@ server_inherit(struct server *src, const char *name, else dst->srv_conf.flags &= ~SRVFLAG_TLS; + /* Don't inherit the "match" option, use it from the alias */ + dst->srv_conf.flags &= ~SRVFLAG_SERVER_MATCH; + dst->srv_conf.flags |= (alias->flags & SRVFLAG_SERVER_MATCH); + if (server_tls_load_keypair(dst) == -1) { yyerror("failed to load public/private keys " "for server %s", dst->srv_conf.name); @@ -1975,7 +1995,8 @@ server_inherit(struct server *src, const char *name, fatal("out of memory"); memcpy(&dstl->srv_conf, &s->srv_conf, sizeof(dstl->srv_conf)); - strlcpy(dstl->srv_conf.name, name, sizeof(dstl->srv_conf.name)); + strlcpy(dstl->srv_conf.name, alias->name, + sizeof(dstl->srv_conf.name)); /* Copy the new Id and listen address */ dstl->srv_conf.id = ++last_server_id; diff --git a/httpd/patterns.7 b/httpd/patterns.7 new file mode 100644 index 0000000..a01ede5 --- /dev/null +++ b/httpd/patterns.7 @@ -0,0 +1,309 @@ +.\" $OpenBSD: patterns.7,v 1.5 2015/06/30 19:01:05 jmc Exp $ +.\" +.\" Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> +.\" Copyright (C) 1994-2015 Lua.org, PUC-Rio. +.\" +.\" Permission is hereby granted, free of charge, to any person obtaining +.\" a copy of this software and associated documentation files (the +.\" "Software"), to deal in the Software without restriction, including +.\" without limitation the rights to use, copy, modify, merge, publish, +.\" distribute, sublicense, and/or sell copies of the Software, and to +.\" permit persons to whom the Software is furnished to do so, subject to +.\" the following conditions: +.\" +.\" The above copyright notice and this permission notice shall be +.\" included in all copies or substantial portions of the Software. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +.\" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +.\" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +.\" IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +.\" CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +.\" TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +.\" SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +.\" +.\" Derived from section 6.4.1 in manual.html of Lua 5.3.1: +.\" $Id: patterns.7,v 1.5 2015/06/30 19:01:05 jmc Exp $ +.\" +.Dd $Mdocdate: June 30 2015 $ +.Dt PATTERNS 7 +.Os +.Sh NAME +.Nm patterns +.Nd Lua's pattern matching rules +.Sh DESCRIPTION +Pattern matching in +.Xr httpd 8 +is based on the implementation of the Lua scripting language and +provides a simple and fast alternative to the regular expressions (REs) that +are described in +.Xr re_format 7 . +Patterns are described by regular strings, which are interpreted as +patterns by the pattern-matching +.Dq find +and +.Dq match +functions. +This document describes the syntax and the meaning (that is, what they +match) of these strings. +.Sh CHARACTER CLASS +A character class is used to represent a set of characters. +The following combinations are allowed in describing a character +class: +.Bl -tag -width Ds +.It Ar x +(where +.Ar x +is not one of the magic characters +.Sq ^$()%.[]*+-? ) +represents the character +.Ar x +itself. +.It . +(a dot) represents all characters. +.It %a +represents all letters. +.It %c +represents all control characters. +.It %d +represents all digits. +.It %g +represents all printable characters except space. +.It %l +represents all lowercase letters. +.It %p +represents all punctuation characters. +.It %s +represents all space characters. +.It %u +represents all uppercase letters. +.It %w +represents all alphanumeric characters. +.It %x +represents all hexadecimal digits. +.It Pf % Ar x +(where +.Ar x +is any non-alphanumeric character) represents the character +.Ar x . +This is the standard way to escape the magic characters. +Any non-alphanumeric character (including all punctuation characters, +even the non-magical) can be preceded by a +.Sq % +when used to represent itself in a pattern. +.It Bq Ar set +represents the class which is the union of all +characters in +.Ar set . +A range of characters can be specified by separating the end +characters of the range, in ascending order, with a +.Sq - . +All classes +.Sq Ar %x +described above can also be used as components in +.Ar set . +All other characters in +.Ar set +represent themselves. +For example, +.Sq [%w_] +(or +.Sq [_%w] ) +represents all alphanumeric characters plus the underscore, +.Sq [0-7] +represents the octal digits, +and +.Sq [0-7%l%-] +represents the octal digits plus the lowercase letters plus the +.Sq - +character. +.Pp +The interaction between ranges and classes is not defined. +Therefore, patterns like +.Sq [%a-z] +or +.Sq [a-%%] +have no meaning. +.It Bq Ar ^set +represents the complement of +.Ar set , +where +.Ar set +is interpreted as above. +.El +.Pp +For all classes represented by single letters ( +.Sq %a , +.Sq %c , +etc.), +the corresponding uppercase letter represents the complement of the class. +For instance, +.Sq %S +represents all non-space characters. +.Pp +The definitions of letter, space, and other character groups depend on +the current locale. +In particular, the class +.Sq [a-z] +may not be equivalent to +.Sq %l . +.Sh PATTERN ITEM +A pattern item can be +.Bl -bullet +.It +a single character class, which matches any single character in the class; +.It +a single character class followed by +.Sq * , +which matches zero or more repetitions of characters in the class. +These repetition items will always match the longest possible sequence; +.It +a single character class followed by +.Sq + , +which matches one or more repetitions of characters in the class. +These repetition items will always match the longest possible sequence; +.It +a single character class followed by +.Sq - , +which also matches zero or more repetitions of characters in the class. +Unlike +.Sq * , +these repetition items will always match the shortest possible sequence; +.It +a single character class followed by +.Sq \&? , +which matches zero or one occurrence of a character in the class. +It always matches one occurrence if possible; +.It +.Sq Pf % Ar n , +for +.Ar n +between 1 and 9; +such item matches a substring equal to the n-th captured string (see below); +.It +.Sq Pf %b Ar xy , +where +.Ar x +and +.Ar y +are two distinct characters; +such item matches strings that start with +.Ar x , +end with +.Ar y , +and where the +.Ar x +and +.Ar y +are +.Em balanced . +This means that if one reads the string from left to right, counting +.Em +1 +for an +.Ar x +and +.Em -1 +for a +.Ar y , +the ending +.Ar y +is the first +.Ar y +where the count reaches 0. +For instance, the item +.Sq %b() +matches expressions with balanced parentheses. +.It +.Sq Pf %f Bq Ar set , +a +.Em frontier pattern ; +such item matches an empty string at any position such that the next +character belongs to +.Ar set +and the previous character does not belong to +.Ar set . +The set +.Ar set +is interpreted as previously described. +The beginning and the end of the subject are handled as if +they were the character +.Sq \e0 . +.El +.Sh PATTERN +A pattern is a sequence of pattern items. +A caret +.Sq ^ +at the beginning of a pattern anchors the match at the beginning of +the subject string. +A +.Sq $ +at the end of a pattern anchors the match at the end of the subject string. +At other positions, +.Sq ^ +and +.Sq $ +have no special meaning and represent themselves. +.Sh CAPTURES +A pattern can contain sub-patterns enclosed in parentheses; they +describe captures. +When a match succeeds, the substrings of the subject string that match +captures are stored (captured) for future use. +Captures are numbered according to their left parentheses. +For instance, in the pattern +.Qq (a*(.)%w(%s*)) , +the part of the string matching +.Qq a*(.)%w(%s*) +is stored as the first capture (and therefore has number 1); +the character matching +.So \. Sc +is captured with number 2, +and the part matching +.Qq %s* +has number 3. +.Pp +As a special case, the empty capture +.Sq () +captures the current string position (a number). +For instance, if we apply the pattern +.Qq ()aa() +on the string +.Qq flaaap , +there will be two captures: 2 and 4. +.Sh SEE ALSO +.Xr fnmatch 3 , +.Xr re_format 7 , +.Xr httpd 8 +.Rs +.%A Roberto Ierusalimschy +.%A Luiz Henrique de Figueiredo +.%A Waldemar Celes +.%Q Lua.org +.%Q PUC-Rio +.%D June 2015 +.%R Lua 5.3 Reference Manual +.%T Patterns +.%U http://www.lua.org/manual/5.3/manual.html#6.4.1 +.Re +.Sh HISTORY +The first implementation of the pattern rules were introduced with Lua 2.5. +Almost twenty years later, +an implementation based on Lua 5.3.1 appeared in +.Ox 5.8 . +.Sh AUTHORS +The pattern matching is derived from the original implementation of +the Lua scripting language written by +.An -nosplit +.An Roberto Ierusalimschy , +.An Waldemar Celes , +and +.An Luiz Henrique de Figueiredo +at PUC-Rio. +It was turned into a native C API for +.Xr httpd 8 +by +.An Reyk Floeter Aq Mt reyk@openbsd.org . +.Sh CAVEATS +A notable difference with the Lua implementation is the position in the string +returned by captures. +It follows the C-style indexing (position starting from 0) +instead of Lua-style indexing (position starting from 1). diff --git a/httpd/patterns.c b/httpd/patterns.c new file mode 100644 index 0000000..27a813f --- /dev/null +++ b/httpd/patterns.c @@ -0,0 +1,712 @@ +/* $OpenBSD: patterns.c,v 1.3 2015/06/26 10:07:48 semarie Exp $ */ + +/* + * Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> + * Copyright (C) 1994-2015 Lua.org, PUC-Rio. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * Derived from Lua 5.3.1: + * $Id: patterns.c,v 1.3 2015/06/26 10:07:48 semarie Exp $ + * Standard library for string operations and pattern-matching + */ + +#include <sys/types.h> +#include <ctype.h> +#include <errno.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +#include "patterns.h" + +#define uchar(c) ((unsigned char)(c)) /* macro to 'unsign' a char */ +#define CAP_UNFINISHED (-1) +#define CAP_POSITION (-2) +#define L_ESC '%' +#define SPECIALS "^$*+?.([%-" + +struct match_state { + int matchdepth; /* control for recursive depth (to avoid C + * stack overflow) */ + int repetitioncounter; /* control the repetition items */ + int maxcaptures; /* configured capture limit */ + const char *src_init; /* init of source string */ + const char *src_end; /* end ('\0') of source string */ + const char *p_end; /* end ('\0') of pattern */ + const char *error; /* should be NULL */ + int level; /* total number of captures (finished or + * unfinished) */ + struct { + const char *init; + ptrdiff_t len; + } capture[MAXCAPTURES]; +}; + +/* recursive function */ +static const char *match(struct match_state *, const char *, const char *); + +static int +match_error(struct match_state *ms, const char *error) +{ + ms->error = ms->error == NULL ? error : ms->error; + return (-1); +} + +static int +check_capture(struct match_state *ms, int l) +{ + l -= '1'; + if (l < 0 || l >= ms->level || ms->capture[l].len == CAP_UNFINISHED) + return match_error(ms, "invalid capture index"); + return (l); +} + +static int +capture_to_close(struct match_state *ms) +{ + int level = ms->level; + for (level--; level >= 0; level--) + if (ms->capture[level].len == CAP_UNFINISHED) + return (level); + return match_error(ms, "invalid pattern capture"); +} + +static const char * +classend(struct match_state *ms, const char *p) +{ + switch (*p++) { + case L_ESC: + if (p == ms->p_end) + match_error(ms, + "malformed pattern (ends with '%')"); + return p + 1; + case '[': + if (*p == '^') + p++; + do { + /* look for a ']' */ + if (p == ms->p_end) { + match_error(ms, + "malformed pattern (missing ']')"); + break; + } + if (*(p++) == L_ESC && p < ms->p_end) { + /* skip escapes (e.g. '%]') */ + p++; + } + } while (*p != ']'); + return p + 1; + default: + return p; + } +} + +static int +match_class(int c, int cl) +{ + int res; + switch (tolower(cl)) { + case 'a': + res = isalpha(c); + break; + case 'c': + res = iscntrl(c); + break; + case 'd': + res = isdigit(c); + break; + case 'g': + res = isgraph(c); + break; + case 'l': + res = islower(c); + break; + case 'p': + res = ispunct(c); + break; + case 's': + res = isspace(c); + break; + case 'u': + res = isupper(c); + break; + case 'w': + res = isalnum(c); + break; + case 'x': + res = isxdigit(c); + break; + default: + return (cl == c); + } + return (islower(cl) ? res : !res); +} + +static int +matchbracketclass(int c, const char *p, const char *ec) +{ + int sig = 1; + if (*(p + 1) == '^') { + sig = 0; + /* skip the '^' */ + p++; + } + while (++p < ec) { + if (*p == L_ESC) { + p++; + if (match_class(c, uchar(*p))) + return sig; + } else if ((*(p + 1) == '-') && (p + 2 < ec)) { + p += 2; + if (uchar(*(p - 2)) <= c && c <= uchar(*p)) + return sig; + } else if (uchar(*p) == c) + return sig; + } + return !sig; +} + +static int +singlematch(struct match_state *ms, const char *s, const char *p, + const char *ep) +{ + if (s >= ms->src_end) + return 0; + else { + int c = uchar(*s); + switch (*p) { + case '.': + /* matches any char */ + return (1); + case L_ESC: + return match_class(c, uchar(*(p + 1))); + case '[': + return matchbracketclass(c, p, ep - 1); + default: + return (uchar(*p) == c); + } + } +} + +static const char * +matchbalance(struct match_state *ms, const char *s, const char *p) +{ + if (p >= ms->p_end - 1) { + match_error(ms, + "malformed pattern (missing arguments to '%b')"); + return (NULL); + } + if (*s != *p) + return (NULL); + else { + int b = *p; + int e = *(p + 1); + int cont = 1; + while (++s < ms->src_end) { + if (*s == e) { + if (--cont == 0) + return s + 1; + } else if (*s == b) + cont++; + } + } + + /* string ends out of balance */ + return (NULL); +} + +static const char * +max_expand(struct match_state *ms, const char *s, const char *p, const char *ep) +{ + ptrdiff_t i = 0; + /* counts maximum expand for item */ + while (singlematch(ms, s + i, p, ep)) + i++; + /* keeps trying to match with the maximum repetitions */ + while (i >= 0) { + const char *res = match(ms, (s + i), ep + 1); + if (res) + return res; + /* else didn't match; reduce 1 repetition to try again */ + i--; + } + return NULL; +} + +static const char * +min_expand(struct match_state *ms, const char *s, const char *p, const char *ep) +{ + for (;;) { + const char *res = match(ms, s, ep + 1); + if (res != NULL) + return res; + else if (singlematch(ms, s, p, ep)) + s++; /* try with one more repetition */ + else + return NULL; + } +} + +static const char * +start_capture(struct match_state *ms, const char *s, const char *p, int what) +{ + const char *res; + + int level = ms->level; + if (level >= ms->maxcaptures) { + match_error(ms, "too many captures"); + return (NULL); + } + ms->capture[level].init = s; + ms->capture[level].len = what; + ms->level = level + 1; + /* undo capture if match failed */ + if ((res = match(ms, s, p)) == NULL) + ms->level--; + return res; +} + +static const char * +end_capture(struct match_state *ms, const char *s, const char *p) +{ + int l = capture_to_close(ms); + const char *res; + if (l == -1) + return NULL; + /* close capture */ + ms->capture[l].len = s - ms->capture[l].init; + /* undo capture if match failed */ + if ((res = match(ms, s, p)) == NULL) + ms->capture[l].len = CAP_UNFINISHED; + return res; +} + +static const char * +match_capture(struct match_state *ms, const char *s, int l) +{ + size_t len; + l = check_capture(ms, l); + if (l == -1) + return NULL; + len = ms->capture[l].len; + if ((size_t) (ms->src_end - s) >= len && + memcmp(ms->capture[l].init, s, len) == 0) + return s + len; + else + return NULL; +} + +static const char * +match(struct match_state *ms, const char *s, const char *p) +{ + const char *ep, *res; + char previous; + + if (ms->matchdepth-- == 0) { + match_error(ms, "pattern too complex"); + return (NULL); + } + + /* using goto's to optimize tail recursion */ + init: + /* end of pattern? */ + if (p != ms->p_end) { + switch (*p) { + case '(': + /* start capture */ + if (*(p + 1) == ')') + /* position capture? */ + s = start_capture(ms, s, p + 2, CAP_POSITION); + else + s = start_capture(ms, s, p + 1, CAP_UNFINISHED); + break; + case ')': + /* end capture */ + s = end_capture(ms, s, p + 1); + break; + case '$': + /* is the '$' the last char in pattern? */ + if ((p + 1) != ms->p_end) { + /* no; go to default */ + goto dflt; + } + /* check end of string */ + s = (s == ms->src_end) ? s : NULL; + break; + case L_ESC: + /* escaped sequences not in the format class[*+?-]? */ + switch (*(p + 1)) { + case 'b': + /* balanced string? */ + s = matchbalance(ms, s, p + 2); + if (s != NULL) { + p += 4; + /* return match(ms, s, p + 4); */ + goto init; + } /* else fail (s == NULL) */ + break; + case 'f': + /* frontier? */ + p += 2; + if (*p != '[') { + match_error(ms, "missing '['" + " after '%f' in pattern"); + break; + } + /* points to what is next */ + ep = classend(ms, p); + if (ms->error != NULL) + break; + previous = + (s == ms->src_init) ? '\0' : *(s - 1); + if (!matchbracketclass(uchar(previous), + p, ep - 1) && + matchbracketclass(uchar(*s), + p, ep - 1)) { + p = ep; + /* return match(ms, s, ep); */ + goto init; + } + /* match failed */ + s = NULL; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + /* capture results (%0-%9)? */ + s = match_capture(ms, s, uchar(*(p + 1))); + if (s != NULL) { + p += 2; + /* return match(ms, s, p + 2) */ + goto init; + } + break; + default: + goto dflt; + } + break; + default: + + /* pattern class plus optional suffix */ + dflt: + /* points to optional suffix */ + ep = classend(ms, p); + if (ms->error != NULL) + break; + + /* does not match at least once? */ + if (!singlematch(ms, s, p, ep)) { + if (ms->repetitioncounter-- == 0) { + match_error(ms, "max repetition items"); + s = NULL; /* fail */ + /* accept empty? */ + } else if + (*ep == '*' || *ep == '?' || *ep == '-') { + p = ep + 1; + /* return match(ms, s, ep + 1); */ + goto init; + } else { + /* '+' or no suffix */ + s = NULL; /* fail */ + } + } else { + /* matched once */ + /* handle optional suffix */ + switch (*ep) { + case '?': + /* optional */ + if ((res = + match(ms, s + 1, ep + 1)) != NULL) + s = res; + else { + /* + * else return + * match(ms, s, ep + 1); + */ + p = ep + 1; + goto init; + } + break; + case '+': + /* 1 or more repetitions */ + s++; /* 1 match already done */ + /* FALLTHROUGH */ + case '*': + /* 0 or more repetitions */ + s = max_expand(ms, s, p, ep); + break; + case '-': + /* 0 or more repetitions (minimum) */ + s = min_expand(ms, s, p, ep); + break; + default: + /* no suffix */ + s++; + p = ep; + /* return match(ms, s + 1, ep); */ + goto init; + } + } + break; + } + } + ms->matchdepth++; + return s; +} + +static const char * +lmemfind(const char *s1, size_t l1, + const char *s2, size_t l2) +{ + const char *init; + + if (l2 == 0) { + /* empty strings are everywhere */ + return (s1); + } else if (l2 > l1) { + /* avoids a negative 'l1' */ + return (NULL); + } else { + /* + * to search for a '*s2' inside 's1' + * - 1st char will be checked by 'memchr' + * - 's2' cannot be found after that + */ + l2--; + l1 = l1 - l2; + while (l1 > 0 && + (init = (const char *)memchr(s1, *s2, l1)) != NULL) { + /* 1st char is already checked */ + init++; + if (memcmp(init, s2 + 1, l2) == 0) + return init - 1; + else { + /* correct 'l1' and 's1' to try again */ + l1 -= init - s1; + s1 = init; + } + } + /* not found */ + return (NULL); + } +} + +static int +push_onecapture(struct match_state *ms, int i, const char *s, + const char *e, struct str_find *sm) +{ + if (i >= ms->level) { + if (i == 0 || ms->level == 0) { + /* add whole match */ + sm->sm_so = (off_t)(s - ms->src_init); + sm->sm_eo = (off_t)(e - s) + sm->sm_so; + } else + return match_error(ms, "invalid capture index"); + } else { + ptrdiff_t l = ms->capture[i].len; + if (l == CAP_UNFINISHED) + return match_error(ms, "unfinished capture"); + sm->sm_so = ms->capture[i].init - ms->src_init; + sm->sm_eo = sm->sm_so + l; + } + sm->sm_eo = sm->sm_eo < sm->sm_so ? sm->sm_so : sm->sm_eo; + return (0); +} + +static int +push_captures(struct match_state *ms, const char *s, const char *e, + struct str_find *sm, size_t nsm) +{ + unsigned int i; + unsigned int nlevels = (ms->level <= 0 && s) ? 1 : ms->level; + + if (nlevels > nsm) + nlevels = nsm; + for (i = 0; i < nlevels; i++) + if (push_onecapture(ms, i, s, e, sm + i) == -1) + break; + + /* number of strings pushed */ + return (nlevels); +} + +/* check whether pattern has no special characters */ +static int +nospecials(const char *p, size_t l) +{ + size_t upto = 0; + + do { + if (strpbrk(p + upto, SPECIALS)) { + /* pattern has a special character */ + return 0; + } + /* may have more after \0 */ + upto += strlen(p + upto) + 1; + } while (upto <= l); + + /* no special chars found */ + return (1); +} + +static int +str_find_aux(struct match_state *ms, const char *pattern, const char *string, + struct str_find *sm, size_t nsm, off_t init) +{ + size_t ls = strlen(string); + size_t lp = strlen(pattern); + const char *s = string; + const char *p = pattern; + const char *s1, *s2; + int anchor, i; + + if (init < 0) + init = 0; + else if (init > (off_t)ls) + return match_error(ms, "starting after string's end"); + s1 = s + init; + + if (nospecials(p, lp)) { + /* do a plain search */ + s2 = lmemfind(s1, ls - (size_t)init, p, lp); + if (s2 != NULL) { + i = 0; + sm[i].sm_so = 0; + sm[i].sm_eo = ls; + if (nsm > 1) { + i++; + sm[i].sm_so = s2 - s; + sm[i].sm_eo = (s2 - s) + lp; + } + return (i + 1); + } + return (0); + } + + anchor = (*p == '^'); + if (anchor) { + p++; + lp--; /* skip anchor character */ + } + ms->maxcaptures = (nsm > MAXCAPTURES ? MAXCAPTURES : nsm) - 1; + ms->matchdepth = MAXCCALLS; + ms->repetitioncounter = MAXREPETITION; + ms->src_init = s; + ms->src_end = s + ls; + ms->p_end = p + lp; + do { + const char *res; + ms->level = 0; + if ((res = match(ms, s1, p)) != NULL) { + sm->sm_so = 0; + sm->sm_eo = ls; + return push_captures(ms, s1, res, sm + 1, nsm - 1) + 1; + + } else if (ms->error != NULL) { + return 0; + } + } while (s1++ < ms->src_end && !anchor); + + return 0; +} + +int +str_find(const char *string, const char *pattern, struct str_find *sm, + size_t nsm, const char **errstr) +{ + struct match_state ms; + int ret; + + memset(&ms, 0, sizeof(ms)); + memset(sm, 0, nsm * sizeof(*sm)); + + ret = str_find_aux(&ms, pattern, string, sm, nsm, 0); + if (ms.error != NULL) { + /* Return 0 on error and store the error string */ + *errstr = ms.error; + ret = 0; + } else + *errstr = NULL; + + return (ret); +} + +int +str_match(const char *string, const char *pattern, struct str_match *m, + const char **errstr) +{ + struct str_find sm[MAXCAPTURES]; + struct match_state ms; + int ret, i; + size_t len, nsm; + + nsm = MAXCAPTURES; + memset(&ms, 0, sizeof(ms)); + memset(sm, 0, sizeof(sm)); + memset(m, 0, sizeof(*m)); + + ret = str_find_aux(&ms, pattern, string, sm, nsm, 0); + if (ret == 0 || ms.error != NULL) { + /* Return -1 on error and store the error string */ + *errstr = ms.error; + return (-1); + } + + if ((m->sm_match = calloc(ret, sizeof(char *))) == NULL) { + *errstr = strerror(errno); + return (-1); + } + m->sm_nmatch = ret; + + for (i = 0; i < ret; i++) { + if (sm[i].sm_so > sm[i].sm_eo) + continue; + len = sm[i].sm_eo - sm[i].sm_so; + if ((m->sm_match[i] = strndup(string + + sm[i].sm_so, len)) == NULL) { + *errstr = strerror(errno); + str_match_free(m); + return (-1); + } + } + + *errstr = NULL; + return (0); +} + +void +str_match_free(struct str_match *m) +{ + unsigned int i = 0; + for (i = 0; i < m->sm_nmatch; i++) + free(m->sm_match[i]); + free(m->sm_match); + m->sm_nmatch = 0; +} diff --git a/httpd/patterns.h b/httpd/patterns.h new file mode 100644 index 0000000..74790b5 --- /dev/null +++ b/httpd/patterns.h @@ -0,0 +1,47 @@ +/* $OpenBSD: patterns.h,v 1.2 2015/06/26 17:26:29 semarie Exp $ */ + +/* + * Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef PATTERNS_H +#define PATTERNS_H + +#include <sys/types.h> +#include <sys/cdefs.h> + +#define MAXCAPTURES 32 /* Max no. of allowed captures in pattern */ +#define MAXCCALLS 200 /* Max recusion depth in pattern matching */ +#define MAXREPETITION 0xfffff /* Max for repetition items */ + +struct str_find { + off_t sm_so; /* start offset of match */ + off_t sm_eo; /* end offset of match */ +}; + +struct str_match { + char **sm_match; /* allocated array of matched strings */ + unsigned int sm_nmatch; /* number of elements in array */ +}; + +__BEGIN_DECLS +int str_find(const char *, const char *, struct str_find *, size_t, + const char **); +int str_match(const char *, const char *, struct str_match *, + const char **); +void str_match_free(struct str_match *); +__END_DECLS + +#endif /* PATTERNS_H */ diff --git a/httpd/server.c b/httpd/server.c index ca67a47..86d0dca 100644 --- a/httpd/server.c +++ b/httpd/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.63 2015/04/23 16:59:28 florian Exp $ */ +/* $OpenBSD: server.c,v 1.70 2015/07/16 16:29:25 florian Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -41,6 +41,7 @@ #include <event.h> #include <imsg.h> #include <tls.h> +#include <vis.h> #include "httpd.h" @@ -312,20 +313,31 @@ server_purge(struct server *srv) void serverconfig_free(struct server_config *srv_conf) { + free(srv_conf->auth); free(srv_conf->return_uri); free(srv_conf->tls_cert_file); - free(srv_conf->tls_cert); free(srv_conf->tls_key_file); - free(srv_conf->tls_key); + + if (srv_conf->tls_cert != NULL) { + explicit_bzero(srv_conf->tls_cert, srv_conf->tls_cert_len); + free(srv_conf->tls_cert); + } + + if (srv_conf->tls_key != NULL) { + explicit_bzero(srv_conf->tls_key, srv_conf->tls_key_len); + free(srv_conf->tls_key); + } } void serverconfig_reset(struct server_config *srv_conf) { - srv_conf->tls_cert_file = srv_conf->tls_key_file = NULL; - srv_conf->tls_cert = srv_conf->tls_key = NULL; - srv_conf->return_uri = NULL; srv_conf->auth = NULL; + srv_conf->return_uri = NULL; + srv_conf->tls_cert = NULL; + srv_conf->tls_cert_file = NULL; + srv_conf->tls_key = NULL; + srv_conf->tls_key_file = NULL; } struct server * @@ -571,6 +583,11 @@ server_tls_readcb(int fd, short event, void *arg) goto err; } + if (len == 0) { + what |= EVBUFFER_EOF; + goto err; + } + if (evbuffer_add(bufev->input, rbuf, len) == -1) { what |= EVBUFFER_ERROR; goto err; @@ -704,7 +721,7 @@ server_input(struct client *clt) /* Adjust write watermark to the socket buffer output size */ bufferevent_setwatermark(clt->clt_bev, EV_WRITE, - clt->clt_sndbufsiz, 0); + SERVER_MIN_PREFETCHED * clt->clt_sndbufsiz, 0); /* Read at most amount of data that fits in one fcgi record. */ bufferevent_setwatermark(clt->clt_bev, EV_READ, 0, FCGI_CONTENT_SIZE); @@ -729,6 +746,10 @@ server_write(struct bufferevent *bev, void *arg) goto done; bufferevent_enable(bev, EV_READ); + + if (clt->clt_srvbev && !(clt->clt_srvbev->enabled & EV_READ)) + bufferevent_enable(clt->clt_srvbev, EV_READ); + return; done: (*bev->errorcb)(bev, EVBUFFER_WRITE|EVBUFFER_EOF, bev->cbarg); @@ -769,6 +790,11 @@ server_read(struct bufferevent *bev, void *arg) goto fail; if (clt->clt_done) goto done; + + if (EVBUFFER_LENGTH(EVBUFFER_OUTPUT(clt->clt_bev)) > (size_t) + SERVER_MAX_PREFETCH * clt->clt_sndbufsiz) + bufferevent_disable(bev, EV_READ); + return; done: (*bev->errorcb)(bev, EVBUFFER_READ|EVBUFFER_EOF, bev->cbarg); @@ -919,7 +945,7 @@ server_accept(int fd, short event, void *arg) close(s); free(clt); /* - * the client struct was not completly set up, but still + * the client struct was not completely set up, but still * counted as an inflight client. account for this. */ server_inflight_dec(NULL, __func__); @@ -954,6 +980,7 @@ server_accept_tls(int fd, short event, void *arg) } else if (ret != 0) { log_warnx("%s: TLS accept failed - %s", __func__, tls_error(srv->srv_tls_ctx)); + server_close(clt, "TLS accept failed"); return; } @@ -1020,7 +1047,7 @@ server_log(struct client *clt, const char *msg) { char ibuf[HOST_NAME_MAX+1], obuf[HOST_NAME_MAX+1]; struct server_config *srv_conf = clt->clt_srv_conf; - char *ptr = NULL; + char *ptr = NULL, *vmsg = NULL; int debug_cmd = -1; extern int verbose; @@ -1050,13 +1077,14 @@ server_log(struct client *clt, const char *msg) if (EVBUFFER_LENGTH(clt->clt_log) && evbuffer_add_printf(clt->clt_log, "\n") != -1) ptr = evbuffer_readline(clt->clt_log); + (void)stravis(&vmsg, msg, HTTPD_LOGVIS); server_sendlog(srv_conf, debug_cmd, "server %s, " "client %d (%d active), %s:%u -> %s, " "%s%s%s", srv_conf->name, clt->clt_id, server_clients, - ibuf, ntohs(clt->clt_port), obuf, msg, + ibuf, ntohs(clt->clt_port), obuf, vmsg == NULL ? "" : vmsg, ptr == NULL ? "" : ",", ptr == NULL ? "" : ptr); - if (ptr != NULL) - free(ptr); + free(vmsg); + free(ptr); } } @@ -1118,6 +1146,9 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg) case IMSG_CFG_SERVER: config_getserver(env, imsg); break; + case IMSG_CFG_TLS: + config_gettls(env, imsg); + break; case IMSG_CFG_DONE: config_getcfg(env, imsg); break; diff --git a/httpd/server_fcgi.c b/httpd/server_fcgi.c index b627dc3..ce0ccc8 100644 --- a/httpd/server_fcgi.c +++ b/httpd/server_fcgi.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server_fcgi.c,v 1.53 2015/03/26 09:01:51 florian Exp $ */ +/* $OpenBSD: server_fcgi.c,v 1.54 2015/06/09 08:50:52 jung Exp $ */ /* * Copyright (c) 2014 Florian Obser <florian@openbsd.org> @@ -32,6 +32,7 @@ #include <time.h> #include <ctype.h> #include <event.h> +#include <unistd.h> #include "httpd.h" #include "http.h" @@ -153,7 +154,9 @@ server_fcgi(struct httpd *env, struct client *clt) goto fail; } + close(clt->clt_fd); clt->clt_fd = fd; + if (clt->clt_srvbev != NULL) bufferevent_free(clt->clt_srvbev); diff --git a/httpd/server_file.c b/httpd/server_file.c index 52e50ff..16b47d0 100644 --- a/httpd/server_file.c +++ b/httpd/server_file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server_file.c,v 1.54 2015/05/05 11:10:13 florian Exp $ */ +/* $OpenBSD: server_file.c,v 1.55 2015/07/16 19:05:28 reyk Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -43,20 +43,20 @@ struct range { off_t end; }; -int server_file_access(struct httpd *, struct client *, +int server_file_access(struct httpd *, struct client *, char *, size_t); -int server_file_request(struct httpd *, struct client *, +int server_file_request(struct httpd *, struct client *, char *, struct stat *); -int server_partial_file_request(struct httpd *, struct client *, +int server_partial_file_request(struct httpd *, struct client *, char *, struct stat *, char *); -int server_file_index(struct httpd *, struct client *, +int server_file_index(struct httpd *, struct client *, struct stat *); int server_file_modified_since(struct http_descriptor *, struct stat *); -int server_file_method(struct client *); -int parse_range_spec(char *, size_t, struct range *); -struct range *parse_range(char *, size_t, int *); -int buffer_add_range(int, struct evbuffer *, struct range *); +int server_file_method(struct client *); +int parse_range_spec(char *, size_t, struct range *); +struct range *parse_range(char *, size_t, int *); +int buffer_add_range(int, struct evbuffer *, struct range *); int server_file_access(struct httpd *env, struct client *clt, @@ -296,9 +296,9 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path, struct range *range; struct evbuffer *evb = NULL; size_t content_length; - int code = 500, fd = -1, i, nranges, ret; + int code = 500, fd = -1, i, nranges, ret; uint32_t boundary; - char content_range[64]; + char content_range[64]; const char *errstr = NULL; /* Ignore range request for methods other than GET */ @@ -325,7 +325,7 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path, if (nranges == 1) { (void)snprintf(content_range, sizeof(content_range), - "bytes %lld-%lld/%lld", range->start, range->end, + "bytes %lld-%lld/%lld", range->start, range->end, st->st_size); if (kv_add(&resp->http_headers, "Content-Range", content_range) == NULL) @@ -347,8 +347,8 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path, content_length += i; if ((i = evbuffer_add_printf(evb, "Content-Type: %s/%s\r\n", - media == NULL ? "application" : media->media_type, - media == NULL ? + media == NULL ? "application" : media->media_type, + media == NULL ? "octet-stream" : media->media_subtype)) == -1) goto abort; @@ -374,10 +374,10 @@ server_partial_file_request(struct httpd *env, struct client *clt, char *path, /* prepare multipart/byteranges media type */ (void)strlcpy(multipart_media.media_type, "multipart", - sizeof(multipart_media.media_type)); + sizeof(multipart_media.media_type)); (void)snprintf(multipart_media.media_subtype, - sizeof(multipart_media.media_subtype), - "byteranges; boundary=%ud", boundary); + sizeof(multipart_media.media_subtype), + "byteranges; boundary=%ud", boundary); media = &multipart_media; } @@ -656,7 +656,7 @@ struct range * parse_range(char *str, size_t file_sz, int *nranges) { static struct range ranges[MAX_RANGES]; - int i = 0; + int i = 0; char *p, *q; /* Extract range unit */ @@ -692,7 +692,7 @@ parse_range(char *str, size_t file_sz, int *nranges) int parse_range_spec(char *str, size_t size, struct range *r) { - size_t start_str_len, end_str_len; + size_t start_str_len, end_str_len; char *p, *start_str, *end_str; const char *errstr; diff --git a/httpd/server_http.c b/httpd/server_http.c index 7b65a5e..9a6609e 100644 --- a/httpd/server_http.c +++ b/httpd/server_http.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server_http.c,v 1.79 2015/05/03 18:39:58 florian Exp $ */ +/* $OpenBSD: server_http.c,v 1.89 2015/07/16 19:05:28 reyk Exp $ */ /* * Copyright (c) 2006 - 2015 Reyk Floeter <reyk@openbsd.org> @@ -29,14 +29,17 @@ #include <string.h> #include <unistd.h> #include <limits.h> +#include <fnmatch.h> #include <stdio.h> #include <time.h> #include <resolv.h> #include <event.h> -#include <fnmatch.h> +#include <ctype.h> +#include <vis.h> #include "httpd.h" #include "http.h" +#include "patterns.h" static int server_httpmethod_cmp(const void *, const void *); static int server_httperror_cmp(const void *, const void *); @@ -633,6 +636,7 @@ server_reset_http(struct client *clt) clt->clt_remote_user = NULL; clt->clt_bev->readcb = server_read_http; clt->clt_srv_conf = &srv->srv_conf; + str_match_free(&clt->clt_srv_match); } ssize_t @@ -738,7 +742,7 @@ server_abort_http(struct client *clt, u_int code, const char *msg) const char *httperr = NULL, *style; char *httpmsg, *body = NULL, *extraheader = NULL; char tmbuf[32], hbuf[128]; - char buf[IBUF_READ_SIZE], *ptr = NULL; + char buf[IBUF_READ_SIZE]; int bodylen; if (code == 0) { @@ -770,16 +774,13 @@ server_abort_http(struct client *clt, u_int code, const char *msg) if (msg == NULL) break; memset(buf, 0, sizeof(buf)); - if ((ptr = server_expand_http(clt, msg, - buf, sizeof(buf))) == NULL) - goto done; - if ((ptr = url_encode(ptr)) == NULL) + if (server_expand_http(clt, msg, buf, sizeof(buf)) == NULL) goto done; - if (asprintf(&extraheader, "Location: %s\r\n", ptr) == -1) { + if (asprintf(&extraheader, "Location: %s\r\n", buf) == -1) { code = 500; extraheader = NULL; } - msg = ptr; + msg = buf; break; case 401: if (asprintf(&extraheader, @@ -858,7 +859,6 @@ server_abort_http(struct client *clt, u_int code, const char *msg) server_close(clt, httpmsg); free(httpmsg); } - free(ptr); } void @@ -877,6 +877,8 @@ server_close_http(struct client *clt) clt->clt_descresp = NULL; free(clt->clt_remote_user); clt->clt_remote_user = NULL; + + str_match_free(&clt->clt_srv_match); } char * @@ -885,20 +887,56 @@ server_expand_http(struct client *clt, const char *val, char *buf, { struct http_descriptor *desc = clt->clt_descreq; struct server_config *srv_conf = clt->clt_srv_conf; - char ibuf[128], *str; + char ibuf[128], *str, *path, *query; + const char *errstr = NULL, *p; + size_t size; + int n, ret; if (strlcpy(buf, val, len) >= len) return (NULL); + /* Find previously matched substrings by index */ + for (p = val; clt->clt_srv_match.sm_nmatch && + (p = strstr(p, "%")) != NULL; p++) { + if (!isdigit(*(p + 1))) + continue; + + /* Copy number, leading '%' char and add trailing \0 */ + size = strspn(p + 1, "0123456789") + 2; + if (size >= sizeof(ibuf)) + return (NULL); + (void)strlcpy(ibuf, p, size); + n = strtonum(ibuf + 1, 0, + clt->clt_srv_match.sm_nmatch - 1, &errstr); + if (errstr != NULL) + return (NULL); + + /* Expand variable with matched value */ + if ((str = url_encode(clt->clt_srv_match.sm_match[n])) == NULL) + return (NULL); + ret = expand_string(buf, len, ibuf, str); + free(str); + if (ret != 0) + return (NULL); + } if (strstr(val, "$DOCUMENT_URI") != NULL) { - if (expand_string(buf, len, "$DOCUMENT_URI", - desc->http_path) != 0) + if ((path = url_encode(desc->http_path)) == NULL) + return (NULL); + ret = expand_string(buf, len, "$DOCUMENT_URI", path); + free(path); + if (ret != 0) return (NULL); } if (strstr(val, "$QUERY_STRING") != NULL) { - if (expand_string(buf, len, "$QUERY_STRING", - desc->http_query == NULL ? "" : - desc->http_query) != 0) + if (desc->http_query == NULL) { + ret = expand_string(buf, len, "$QUERY_STRING", ""); + } else { + if ((query = url_encode(desc->http_query)) == NULL) + return (NULL); + ret = expand_string(buf, len, "$QUERY_STRING", query); + free(query); + } + if (ret != 0) return (NULL); } if (strstr(val, "$REMOTE_") != NULL) { @@ -919,27 +957,39 @@ server_expand_http(struct client *clt, const char *val, char *buf, } if (strstr(val, "$REMOTE_USER") != NULL) { if ((srv_conf->flags & SRVFLAG_AUTH) && - clt->clt_remote_user != NULL) - str = clt->clt_remote_user; - else - str = ""; - if (expand_string(buf, len, - "$REMOTE_USER", str) != 0) + clt->clt_remote_user != NULL) { + if ((str = url_encode(clt->clt_remote_user)) + == NULL) + return (NULL); + } else + str = strdup(""); + ret = expand_string(buf, len, "$REMOTE_USER", str); + free(str); + if (ret != 0) return (NULL); } } if (strstr(val, "$REQUEST_URI") != NULL) { + if ((path = url_encode(desc->http_path)) == NULL) + return (NULL); if (desc->http_query == NULL) { - if ((str = strdup(desc->http_path)) == NULL) + str = path; + } else { + if ((query = url_encode(desc->http_query)) == NULL) { + free(path); + return (NULL); + } + ret = asprintf(&str, "%s?%s", path, query); + free(path); + free(query); + if (ret == -1) return (NULL); - } else if (asprintf(&str, "%s?%s", - desc->http_path, desc->http_query) == -1) - return (NULL); - if (expand_string(buf, len, "$REQUEST_URI", str) != 0) { - free(str); - return (NULL); } + + ret = expand_string(buf, len, "$REQUEST_URI", str); free(str); + if (ret != 0) + return (NULL); } if (strstr(val, "$SERVER_") != NULL) { if (strstr(val, "$SERVER_ADDR") != NULL) { @@ -958,8 +1008,12 @@ server_expand_http(struct client *clt, const char *val, char *buf, return (NULL); } if (strstr(val, "$SERVER_NAME") != NULL) { - if (expand_string(buf, len, - "$SERVER_NAME", srv_conf->name) != 0) + if ((str = url_encode(srv_conf->name)) + == NULL) + return (NULL); + ret = expand_string(buf, len, "$SERVER_NAME", str); + free(str); + if (ret != 0) return (NULL); } } @@ -977,8 +1031,10 @@ server_response(struct httpd *httpd, struct client *clt) struct server *srv = clt->clt_srv; struct server_config *srv_conf = &srv->srv_conf; struct kv *kv, key, *host; - int portval = -1; + struct str_find sm; + int portval = -1, ret; char *hostval; + const char *errstr = NULL; /* Canonicalize the request path */ if (desc->http_path == NULL || @@ -1038,9 +1094,17 @@ server_response(struct httpd *httpd, struct client *clt) hostname); } #endif - if ((srv_conf->flags & SRVFLAG_LOCATION) == 0 && - fnmatch(srv_conf->name, hostname, - FNM_CASEFOLD) == 0 && + if (srv_conf->flags & SRVFLAG_LOCATION) + continue; + else if (srv_conf->flags & SRVFLAG_SERVER_MATCH) { + str_find(hostname, srv_conf->name, + &sm, 1, &errstr); + ret = errstr == NULL ? 0 : -1; + } else { + ret = fnmatch(srv_conf->name, + hostname, FNM_CASEFOLD); + } + if (ret == 0 && (portval == -1 || (portval != -1 && portval == srv_conf->port))) { /* Replace host configuration */ @@ -1110,6 +1174,8 @@ server_getlocation(struct client *clt, const char *path) { struct server *srv = clt->clt_srv; struct server_config *srv_conf = clt->clt_srv_conf, *location; + const char *errstr = NULL; + int ret; /* Now search for the location */ TAILQ_FOREACH(location, &srv->srv_hosts, entry) { @@ -1120,11 +1186,20 @@ server_getlocation(struct client *clt, const char *path) } #endif if ((location->flags & SRVFLAG_LOCATION) && - location->parent_id == srv_conf->parent_id && - fnmatch(location->location, path, FNM_CASEFOLD) == 0) { - /* Replace host configuration */ - clt->clt_srv_conf = srv_conf = location; - break; + location->parent_id == srv_conf->parent_id) { + errstr = NULL; + if (location->flags & SRVFLAG_LOCATION_MATCH) { + ret = str_match(path, location->location, + &clt->clt_srv_match, &errstr); + } else { + ret = fnmatch(location->location, + path, FNM_CASEFOLD); + } + if (ret == 0 && errstr == NULL) { + /* Replace host configuration */ + clt->clt_srv_conf = srv_conf = location; + break; + } } } @@ -1133,7 +1208,7 @@ server_getlocation(struct client *clt, const char *path) int server_response_http(struct client *clt, u_int code, - struct media_type *media, size_t size, time_t mtime) + struct media_type *media, off_t size, time_t mtime) { struct http_descriptor *desc = clt->clt_descreq; struct http_descriptor *resp = clt->clt_descresp; @@ -1174,7 +1249,7 @@ server_response_http(struct client *clt, u_int code, /* Set content length, if specified */ if ((cl = kv_add(&resp->http_headers, "Content-Length", NULL)) == NULL || - kv_set(cl, "%ld", size) == -1) + kv_set(cl, "%lld", (long long)size) == -1) return (-1); /* Set last modification time */ @@ -1352,6 +1427,13 @@ server_log_http(struct client *clt, u_int code, size_t len) struct tm *tm; struct server_config *srv_conf; struct http_descriptor *desc; + int ret = -1; + char *user = NULL; + char *path = NULL; + char *query = NULL; + char *version = NULL; + char *referrer_v = NULL; + char *agent_v = NULL; if ((srv_conf = clt->clt_srv_conf) == NULL) return (-1); @@ -1380,18 +1462,34 @@ server_log_http(struct client *clt, u_int code, size_t len) */ switch (srv_conf->logformat) { case LOG_FORMAT_COMMON: - if (evbuffer_add_printf(clt->clt_log, + /* Use vis to encode input values from the header */ + if (clt->clt_remote_user && + stravis(&user, clt->clt_remote_user, HTTPD_LOGVIS) == -1) + goto done; + if (desc->http_version && + stravis(&version, desc->http_version, HTTPD_LOGVIS) == -1) + goto done; + + /* The following should be URL-encoded */ + if (desc->http_path && + (path = url_encode(desc->http_path)) == NULL) + goto done; + if (desc->http_query && + (query = url_encode(desc->http_query)) == NULL) + goto done; + + ret = evbuffer_add_printf(clt->clt_log, "%s %s - %s [%s] \"%s %s%s%s%s%s\" %03d %zu\n", srv_conf->name, ip, clt->clt_remote_user == NULL ? "-" : - clt->clt_remote_user, tstamp, + user, tstamp, server_httpmethod_byid(desc->http_method), - desc->http_path == NULL ? "" : desc->http_path, + desc->http_path == NULL ? "" : path, desc->http_query == NULL ? "" : "?", - desc->http_query == NULL ? "" : desc->http_query, + desc->http_query == NULL ? "" : query, desc->http_version == NULL ? "" : " ", - desc->http_version == NULL ? "" : desc->http_version, - code, len) == -1) - return (-1); + desc->http_version == NULL ? "" : version, + code, len); + break; case LOG_FORMAT_COMBINED: @@ -1405,29 +1503,64 @@ server_log_http(struct client *clt, u_int code, size_t len) agent->kv_value == NULL) agent = NULL; - if (evbuffer_add_printf(clt->clt_log, + /* Use vis to encode input values from the header */ + if (clt->clt_remote_user && + stravis(&user, clt->clt_remote_user, HTTPD_LOGVIS) == -1) + goto done; + if (desc->http_version && + stravis(&version, desc->http_version, HTTPD_LOGVIS) == -1) + goto done; + if (agent && + stravis(&agent_v, agent->kv_value, HTTPD_LOGVIS) == -1) + goto done; + + /* The following should be URL-encoded */ + if (desc->http_path && + (path = url_encode(desc->http_path)) == NULL) + goto done; + if (desc->http_query && + (query = url_encode(desc->http_query)) == NULL) + goto done; + if (referrer && + (referrer_v = url_encode(referrer->kv_value)) == NULL) + goto done; + + ret = evbuffer_add_printf(clt->clt_log, "%s %s - %s [%s] \"%s %s%s%s%s%s\"" " %03d %zu \"%s\" \"%s\"\n", srv_conf->name, ip, clt->clt_remote_user == NULL ? "-" : - clt->clt_remote_user, tstamp, + user, tstamp, server_httpmethod_byid(desc->http_method), - desc->http_path == NULL ? "" : desc->http_path, + desc->http_path == NULL ? "" : path, desc->http_query == NULL ? "" : "?", - desc->http_query == NULL ? "" : desc->http_query, + desc->http_query == NULL ? "" : query, desc->http_version == NULL ? "" : " ", - desc->http_version == NULL ? "" : desc->http_version, + desc->http_version == NULL ? "" : version, code, len, - referrer == NULL ? "" : referrer->kv_value, - agent == NULL ? "" : agent->kv_value) == -1) - return (-1); + referrer == NULL ? "" : referrer_v, + agent == NULL ? "" : agent_v); + break; case LOG_FORMAT_CONNECTION: - if (evbuffer_add_printf(clt->clt_log, " [%s]", - desc->http_path == NULL ? "" : desc->http_path) == -1) - return (-1); + /* URL-encode the path */ + if (desc->http_path && + (path = url_encode(desc->http_path)) == NULL) + goto done; + + ret = evbuffer_add_printf(clt->clt_log, " [%s]", + desc->http_path == NULL ? "" : path); + break; } - return (0); +done: + free(user); + free(path); + free(query); + free(version); + free(referrer_v); + free(agent_v); + + return (ret); } diff --git a/regress/Makefile b/regress/Makefile new file mode 100644 index 0000000..964e6d5 --- /dev/null +++ b/regress/Makefile @@ -0,0 +1,5 @@ +# $OpenBSD: Makefile,v 1.2 2015/07/16 16:35:57 reyk Exp $ + +SUBDIR += patterns tests + +.include <bsd.subdir.mk> diff --git a/regress/patterns/Makefile b/regress/patterns/Makefile new file mode 100644 index 0000000..fe1b348 --- /dev/null +++ b/regress/patterns/Makefile @@ -0,0 +1,32 @@ +# $OpenBSD: Makefile,v 1.2 2015/06/23 19:33:06 reyk Exp $ + +HTTPDSRC = ${.CURDIR}/../../../../usr.sbin/httpd + +.PATH: ${HTTPDSRC} + +REGRESS_TARGETS= test-patterns + +CLEANFILES += patterns-tester + +#LUA?= lua53 +.ifdef LUA +REGRESS_TARGETS+= test-patterns-lua +.endif + +patterns-tester: patterns-tester.c patterns.c patterns.h + ${CC} -o $@ ${CFLAGS} ${.CURDIR}/patterns-tester.c ${HTTPDSRC}/patterns.c -I${HTTPDSRC} + +test-patterns: patterns-tester test-patterns.out test-patterns.in + cat ${.CURDIR}/test-patterns.in | grep -v '^#' | \ + while IFS=' ' read string pattern comments ; do \ + ./patterns-tester "$${string}" "$${pattern}" 2>&1 || true; \ + done | diff -I 'OpenBSD' -u ${.CURDIR}/test-patterns.out - + +test-patterns-lua: patterns-tester.lua test-patterns-lua.out test-patterns.in + cat ${.CURDIR}/test-patterns.in | grep -v '^#' | \ + while IFS=' ' read string pattern comments ; do \ + ${LUA} ${.CURDIR}/patterns-tester.lua "$${string}" "$${pattern}" 2>&1 || true; \ + done | sed "s/.*\/patterns\-tester\.lua/X_PATTERNS_TESTER_X/g" | \ + diff -I 'OpenBSD' -u ${.CURDIR}/test-patterns-lua.out - + +.include <bsd.regress.mk> diff --git a/regress/patterns/patterns-tester.c b/regress/patterns/patterns-tester.c new file mode 100644 index 0000000..54f1456 --- /dev/null +++ b/regress/patterns/patterns-tester.c @@ -0,0 +1,98 @@ +/* $OpenBSD: patterns-tester.c,v 1.1 2015/06/23 18:03:09 semarie Exp $ */ +/* + * Copyright (c) 2015 Sebastien Marie <semarie@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <err.h> +#include <string.h> + +#include "patterns.h" + +extern char * malloc_options; + +static void read_string(char *, size_t); +static void read_string_stop(void); + +static void +read_string(char *buf, size_t len) +{ + size_t i; + + /* init */ + bzero(buf, len); + + /* read */ + if (fgets(buf, len, stdin) == NULL) + err(1, "fgets"); + + /* strip '\n' */ + i = strnlen(buf, len); + if (i != 0) + buf[i-1] = '\0'; +} + +static void +read_string_stop() +{ + if (getchar() != EOF) + errx(1, "read_string_stop: too many input"); +} + +int +main(int argc, char *argv[]) +{ + char string[1024]; + char pattern[1024]; + struct str_match m; + const char *errstr = NULL; + int ret; + size_t i; + + /* configure malloc */ + malloc_options = "S"; + + /* read testcase */ + if (argc != 3) { + /* from stdin (useful for afl) */ + read_string(string, sizeof(string)); + read_string(pattern, sizeof(pattern)); + read_string_stop(); + } else { + /* from arguments */ + strlcpy(string, argv[1], sizeof(string)); + strlcpy(pattern, argv[2], sizeof(pattern)); + } + + /* print testcase */ + printf("string='%s'\n", string); + printf("pattern='%s'\n", pattern); + + /* test it ! */ + ret = str_match(string, pattern, &m, &errstr); + if (errstr != NULL) + errx(1, "str_match: %s", errstr); + + /* print result */ + printf("ret=%d num=%d\n", ret, m.sm_nmatch); + for (i=0; i<m.sm_nmatch; i++) { + printf("%ld: %s\n", i, m.sm_match[i]); + } + + str_match_free(&m); + + return (EXIT_SUCCESS); +} diff --git a/regress/patterns/patterns-tester.lua b/regress/patterns/patterns-tester.lua new file mode 100644 index 0000000..5bc476a --- /dev/null +++ b/regress/patterns/patterns-tester.lua @@ -0,0 +1,3 @@ +-- $OpenBSD: patterns-tester.lua,v 1.1 2015/06/23 18:03:09 semarie Exp $ +print(string.format("string='%s'\npattern='%s'", arg[1], arg[2])); +print(string.match(arg[1], arg[2])); diff --git a/regress/patterns/test-patterns-lua.out b/regress/patterns/test-patterns-lua.out new file mode 100644 index 0000000..b154925 --- /dev/null +++ b/regress/patterns/test-patterns-lua.out @@ -0,0 +1,127 @@ +string='/page/51' +pattern='^/(%a+)/(%d+)$' +page 51 +string='/Apage/51' +pattern='/[^%d][%w%u][^%c]+()[%d]+' +9 +string='/^page/51' +pattern='/^(.a.e)/(.)' +page 5 +string='/page/page-51' +pattern='/(.*)/%1-(%d+)' +page 51 +string='/page/[51]' +pattern='/page/(%b[])' +[51] +string=':-]' +pattern=']+' +] +string=':-)' +pattern='[)]+' +) +string='/page/51' +pattern='$^' +nil +string='1234567890' +pattern='([2-5]-)' + +string='****' +pattern='^**$' +**** +string='xxxx' +pattern='^x*$' +xxxx +string='/page/51' +pattern='no-%d-match' +nil +string='/page/page-51' +pattern='/(.*)/%9-(%d+)' +X_PATTERNS_TESTER_X:3: invalid capture index %9 +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string=':-)' +pattern=')+' +X_PATTERNS_TESTER_X:3: invalid pattern capture +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='/page/51(' +X_PATTERNS_TESTER_X:3: unfinished capture +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='/page/51%' +X_PATTERNS_TESTER_X:3: malformed pattern (ends with '%') +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='/page/[51' +X_PATTERNS_TESTER_X:3: malformed pattern (missing ']') +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/(51)' +pattern='/page/%b(' +X_PATTERNS_TESTER_X:3: malformed pattern (missing arguments to '%b') +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()' +X_PATTERNS_TESTER_X:3: too many captures +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='/page/%f' +X_PATTERNS_TESTER_X:3: missing '[' after '%f' in pattern +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='/page/51' +pattern='/page%f/51' +X_PATTERNS_TESTER_X:3: missing '[' after '%f' in pattern +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='q*********************************' +pattern='*************************************q' +X_PATTERNS_TESTER_X:3: max repetition items +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='q+++++++++++++++++++++++++++++++++' +pattern='+++++++++++++++++++++++++++++++++++++q' +X_PATTERNS_TESTER_X:3: max repetition items +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='q---------------------------------' +pattern='-------------------------------------q' +X_PATTERNS_TESTER_X:3: max repetition items +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? +string='q?????????????????????????????????' +pattern='?????????????????????????????????????q' +X_PATTERNS_TESTER_X:3: max repetition items +stack traceback: + [C]: in function 'string.match' +X_PATTERNS_TESTER_X:3: in main chunk + [C]: in ? diff --git a/regress/patterns/test-patterns.in b/regress/patterns/test-patterns.in new file mode 100644 index 0000000..a988d81 --- /dev/null +++ b/regress/patterns/test-patterns.in @@ -0,0 +1,27 @@ +# $OpenBSD: test-patterns.in,v 1.1 2015/06/23 18:03:09 semarie Exp $ +# string pattern comments +/page/51 ^/(%a+)/(%d+)$ +/Apage/51 /[^%d][%w%u][^%c]+()[%d]+ +/^page/51 /^(.a.e)/(.) +/page/page-51 /(.*)/%1-(%d+) +/page/[51] /page/(%b[]) +:-] ]+ +:-) [)]+ +/page/51 $^ +1234567890 ([2-5]-) +**** ^**$ equiv '[*]*' +xxxx ^x*$ same as before +/page/51 no-%d-match no match +/page/page-51 /(.*)/%9-(%d+) invalid capture index +:-) )+ invalid pattern capture +/page/51 /page/51( unfinished capture +/page/51 /page/51% malformed pattern (ends with '%') +/page/51 /page/[51 malformed pattern (missing ']') +/page/(51) /page/%b( malformed pattern (missing arguments to '%b') +/page/51 ()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()() too many captures +/page/51 /page/%f missing '[' after '%f' in pattern +/page/51 /page%f/51 missing '[' after '%f' in pattern +q********************************* *************************************q max repetition items +q+++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++q max repetition items +q--------------------------------- -------------------------------------q max repetition items +q????????????????????????????????? ?????????????????????????????????????q max repetition items diff --git a/regress/patterns/test-patterns.out b/regress/patterns/test-patterns.out new file mode 100644 index 0000000..9c56279 --- /dev/null +++ b/regress/patterns/test-patterns.out @@ -0,0 +1,99 @@ +# $OpenBSD: test-patterns.out,v 1.2 2015/06/26 10:09:42 semarie Exp $ +string='/page/51' +pattern='^/(%a+)/(%d+)$' +ret=0 num=3 +0: /page/51 +1: page +2: 51 +string='/Apage/51' +pattern='/[^%d][%w%u][^%c]+()[%d]+' +ret=0 num=2 +0: /Apage/51 +1: +string='/^page/51' +pattern='/^(.a.e)/(.)' +ret=0 num=3 +0: /^page/51 +1: page +2: 5 +string='/page/page-51' +pattern='/(.*)/%1-(%d+)' +ret=0 num=3 +0: /page/page-51 +1: page +2: 51 +string='/page/[51]' +pattern='/page/(%b[])' +ret=0 num=2 +0: /page/[51] +1: [51] +string=':-]' +pattern=']+' +ret=0 num=2 +0: :-] +1: ] +string=':-)' +pattern='[)]+' +ret=0 num=2 +0: :-) +1: ) +string='/page/51' +pattern='$^' +ret=-1 num=0 +string='1234567890' +pattern='([2-5]-)' +ret=0 num=2 +0: 1234567890 +1: +string='****' +pattern='^**$' +ret=0 num=2 +0: **** +1: **** +string='xxxx' +pattern='^x*$' +ret=0 num=2 +0: xxxx +1: xxxx +string='/page/51' +pattern='no-%d-match' +ret=-1 num=0 +patterns-tester: str_match: invalid capture index +string='/page/page-51' +pattern='/(.*)/%9-(%d+)' +patterns-tester: str_match: invalid pattern capture +string=':-)' +pattern=')+' +patterns-tester: str_match: unfinished capture +string='/page/51' +pattern='/page/51(' +patterns-tester: str_match: malformed pattern (ends with '%') +string='/page/51' +pattern='/page/51%' +patterns-tester: str_match: malformed pattern (missing ']') +string='/page/51' +pattern='/page/[51' +patterns-tester: str_match: malformed pattern (missing arguments to '%b') +string='/page/(51)' +pattern='/page/%b(' +patterns-tester: str_match: too many captures +string='/page/51' +pattern='()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()' +patterns-tester: str_match: missing '[' after '%f' in pattern +string='/page/51' +pattern='/page/%f' +patterns-tester: str_match: missing '[' after '%f' in pattern +string='/page/51' +pattern='/page%f/51' +patterns-tester: str_match: max repetition items +string='q*********************************' +pattern='*************************************q' +patterns-tester: str_match: max repetition items +string='q+++++++++++++++++++++++++++++++++' +pattern='+++++++++++++++++++++++++++++++++++++q' +patterns-tester: str_match: max repetition items +string='q---------------------------------' +pattern='-------------------------------------q' +patterns-tester: str_match: max repetition items +string='q?????????????????????????????????' +pattern='?????????????????????????????????????q' diff --git a/regress/tests/Client.pm b/regress/tests/Client.pm new file mode 100644 index 0000000..9c27296 --- /dev/null +++ b/regress/tests/Client.pm @@ -0,0 +1,75 @@ +# $OpenBSD: Client.pm,v 1.1 2015/07/16 16:35:57 reyk Exp $ + +# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org> +# Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use strict; +use warnings; + +package Client; +use parent 'Proc'; +use Carp; +use Socket; +use Socket6; +use IO::Socket; +use IO::Socket::INET6; +use IO::Socket::SSL; + +sub new { + my $class = shift; + my %args = @_; + $args{chroot} ||= "."; + $args{logfile} ||= $args{chroot}."/client.log"; + $args{up} ||= "Connected"; + $args{timefile} //= "time.log"; + my $self = Proc::new($class, %args); + $self->{connectdomain} + or croak "$class connect domain not given"; + $self->{connectaddr} + or croak "$class connect addr not given"; + $self->{connectport} + or croak "$class connect port not given"; + return $self; +} + +sub child { + my $self = shift; + + # in case we redo the connect, shutdown the old one + shutdown(\*STDOUT, SHUT_WR); + delete $self->{cs}; + + $SSL_ERROR = ""; + my $iosocket = $self->{tls} ? "IO::Socket::SSL" : "IO::Socket::INET6"; + my $cs = $iosocket->new( + Proto => "tcp", + Domain => $self->{connectdomain}, + PeerAddr => $self->{connectaddr}, + PeerPort => $self->{connectport}, + SSL_verify_mode => SSL_VERIFY_NONE, + ) or die ref($self), " $iosocket socket connect failed: $!,$SSL_ERROR"; + print STDERR "connect sock: ",$cs->sockhost()," ",$cs->sockport(),"\n"; + print STDERR "connect peer: ",$cs->peerhost()," ",$cs->peerport(),"\n"; + if ($self->{tls}) { + print STDERR "tls version: ",$cs->get_sslversion(),"\n"; + print STDERR "tls cipher: ",$cs->get_cipher(),"\n"; + print STDERR "tls peer certificate:\n", + $cs->dump_peer_certificate(); + } + + *STDIN = *STDOUT = $self->{cs} = $cs; +} + +1; diff --git a/regress/tests/Httpd.pm b/regress/tests/Httpd.pm new file mode 100644 index 0000000..d5b9df0 --- /dev/null +++ b/regress/tests/Httpd.pm @@ -0,0 +1,93 @@ +# $OpenBSD: Httpd.pm,v 1.1 2015/07/16 16:35:57 reyk Exp $ + +# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org> +# Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use strict; +use warnings; + +package Httpd; +use parent 'Proc'; +use Carp; +use File::Basename; + +sub new { + my $class = shift; + my %args = @_; + $args{chroot} ||= "."; + $args{logfile} ||= $args{chroot}."/httpd.log"; + $args{up} ||= $args{dryrun} || "server_launch: "; + $args{down} ||= $args{dryrun} ? "httpd.conf:" : "parent terminating"; + $args{func} = sub { Carp::confess "$class func may not be called" }; + $args{conffile} ||= "httpd.conf"; + my $self = Proc::new($class, %args); + ref($self->{http}) eq 'ARRAY' + or $self->{http} = [ split("\n", $self->{http} || "") ]; + $self->{listenaddr} + or croak "$class listen addr not given"; + $self->{listenport} + or croak "$class listen port not given"; + + my $test = basename($self->{testfile} || ""); + # tls does not allow a too long session id, so truncate it + substr($test, 25, length($test) - 25, "") if length($test) > 25; + open(my $fh, '>', $self->{conffile}) + or die ref($self), " conf file $self->{conffile} create failed: $!"; + + # substitute variables in config file + my $curdir = dirname($0) || "."; + my $connectport = $self->{connectport}; + my $connectaddr = $self->{connectaddr}; + my $listenaddr = $self->{listenaddr}; + my $listenport = $self->{listenport}; + + print $fh "prefork 1\n"; # only crashes of first child are observed + print $fh "chroot \"".$args{chroot}."\"\n"; + print $fh "logdir \"".$args{chroot}."\"\n"; + + my @http = @{$self->{http}}; + print $fh "server \"www.$test.local\" {"; + my $tls = $self->{listentls} ? "tls " : ""; + print $fh "\n\tlisten on $self->{listenaddr} ". + "${tls}port $self->{listenport}" unless grep { /^listen / } @http; + # substitute variables in config file + foreach (@http) { + s/(\$[a-z]+)/$1/eeg; + } + print $fh map { "\n\t$_" } @http; + if ($self->{listentls}) { + print $fh "\n"; + print $fh "\ttls certificate \"".$args{chroot}."/server.crt\"\n"; + print $fh "\ttls key \"".$args{chroot}."/server.key\""; + } + print $fh "\n\troot \"/\""; + print $fh "\n\tlog style combined"; + print $fh "\n}\n"; + + return $self; +} + +sub child { + my $self = shift; + my @sudo = $ENV{SUDO} ? $ENV{SUDO} : (); + my @ktrace = $ENV{KTRACE} ? ($ENV{KTRACE}, "-i") : (); + my $httpd = $ENV{HTTPD} ? $ENV{HTTPD} : "httpd"; + my @cmd = (@sudo, @ktrace, $httpd, "-dvv", "-f", $self->{conffile}); + print STDERR "execute: @cmd\n"; + exec @cmd; + die ref($self), " exec '@cmd' failed: $!"; +} + +1; diff --git a/regress/tests/LICENSE b/regress/tests/LICENSE new file mode 100644 index 0000000..8f60827 --- /dev/null +++ b/regress/tests/LICENSE @@ -0,0 +1,14 @@ +# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org> +# Copyright (c) 2014,2015 Reyk Floeter <reyk@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/regress/tests/Makefile b/regress/tests/Makefile new file mode 100644 index 0000000..5298c7c --- /dev/null +++ b/regress/tests/Makefile @@ -0,0 +1,105 @@ +# $OpenBSD: Makefile,v 1.2 2015/07/16 17:00:41 reyk Exp $ + +# The following ports must be installed for the regression tests: +# p5-IO-Socket-INET6 object interface for AF_INET and AF_INET6 domain sockets +# p5-Socket6 Perl defines relating to AF_INET6 sockets +# p5-IO-Socket-SSL perl interface to SSL sockets +# +# Check wether all required perl packages are installed. If some +# are missing print a warning and skip the tests, but do not fail. + +PERL_REQUIRE != perl -Mstrict -Mwarnings -e ' \ + eval { require IO::Socket::INET6 } or print $@; \ + eval { require Socket6 } or print $@; \ + eval { require IO::Socket::SSL } or print $@; \ +' +.if ! empty (PERL_REQUIRE) +regress: + @echo "${PERL_REQUIRE}" + @echo install these perl packages for additional tests +.endif + +# Fill out these variables if you want to test httpd with +# the httpd process running on a remote machine. You have to specify +# a local and remote ip address for the tcp connections. To control +# the remote machine you need a hostname for ssh to log in. All the +# test files must be in the same directory local and remote. + +LOCAL_ADDR ?= +REMOTE_ADDR ?= +REMOTE_SSH ?= + +# Automatically generate regress targets from test cases in directory. + +ARGS != cd ${.CURDIR} && ls args-*.pl +TARGETS ?= ${ARGS} +REGRESS_TARGETS = ${TARGETS:S/^/run-regress-/} +CLEANFILES += *.log httpd.conf ktrace.out stamp-* +CLEANFILES += *.pem *.req *.crt *.key *.srl md5-* + +HTDOCS = 512 1048576 1073741824 +HTDOCS_MD5 = ${HTDOCS:S/^/${.OBJDIR}\/md5-/} +HTDOCS_SPARSE = yes +CLEANFILES += ${HTDOCS} + +# Set variables so that make runs with and without obj directory. +# Only do that if necessary to keep visible output short. + +.if ${.CURDIR} == ${.OBJDIR} +PERLINC = +PERLPATH = +.else +PERLINC = -I${.CURDIR} +PERLPATH = ${.CURDIR}/ +.endif + +# The arg tests take a perl hash with arguments controlling the +# test parameters. Generally they consist of client, httpd, server. + +.for a in ${ARGS} +run-regress-$a: $a ${HTDOCS_MD5} + @echo '\n======== $@ ========' + time SUDO=${SUDO} KTRACE=${KTRACE} HTTPD=${HTTPD} perl ${PERLINC} ${PERLPATH}httpd.pl ${.OBJDIR} ${PERLPATH}$a +.endfor + +# htdocs + +.for d in ${HTDOCS} +${.OBJDIR}/$d: + @echo '\n======== file: $d ========' +.if (${HTDOCS_SPARSE} != "yes") + @dd if=/dev/arandom of=$@ count=$$(($d / 512)) bs=512 +.else + @dd of=$@ seek=$$(($d / 512)) bs=512 count=0 status=none +.endif + +${.OBJDIR}/md5-$d: ${.OBJDIR}/$d + @md5 -q ${.OBJDIR}/$d > $@ +.endfor + +# create certificates for TLS + +ca.crt: + openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=ca/CN=root/ -nodes -newkey rsa -keyout ca.key -x509 -out ca.crt + +server.req: + openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=server/CN=localhost/ -nodes -newkey rsa -keyout server.key -out server.req + +server.crt: ca.crt server.req + openssl x509 -CAcreateserial -CAkey ca.key -CA ca.crt -req -in server.req -out server.crt + +${REGRESS_TARGETS:M*tls*} ${REGRESS_TARGETS:M*https*}: server.crt + +# make perl syntax check for all args files + +.PHONY: syntax + +syntax: stamp-syntax + +stamp-syntax: ${ARGS} +.for a in ${ARGS} + @perl -c ${PERLPATH}$a +.endfor + @date >$@ + +.include <bsd.regress.mk> diff --git a/regress/tests/Proc.pm b/regress/tests/Proc.pm new file mode 100644 index 0000000..8f46012 --- /dev/null +++ b/regress/tests/Proc.pm @@ -0,0 +1,200 @@ +# $OpenBSD: Proc.pm,v 1.1 2015/07/16 16:35:57 reyk Exp $ + +# Copyright (c) 2010-2014 Alexander Bluhm <bluhm@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use strict; +use warnings; + +package Proc; +use Carp; +use Errno; +use File::Basename; +use IO::File; +use POSIX; +use Time::HiRes qw(time alarm sleep); + +my %CHILDREN; + +sub kill_children { + my @pids = @_ ? @_ : keys %CHILDREN + or return; + my @perms; + foreach my $pid (@pids) { + if (kill(TERM => $pid) != 1 and $!{EPERM}) { + push @perms, $pid; + } + } + if (my $sudo = $ENV{SUDO} and @perms) { + local $?; # do not modify during END block + my @cmd = ($sudo, '/bin/kill', '-TERM', @perms); + system(@cmd); + } + delete @CHILDREN{@pids}; +} + +BEGIN { + $SIG{TERM} = $SIG{INT} = sub { + my $sig = shift; + kill_children(); + $SIG{TERM} = $SIG{INT} = 'DEFAULT'; + POSIX::raise($sig); + }; +} + +END { + kill_children(); + $SIG{TERM} = $SIG{INT} = 'DEFAULT'; +} + +sub new { + my $class = shift; + my $self = { @_ }; + $self->{down} ||= "Shutdown"; + $self->{func} && ref($self->{func}) eq 'CODE' + or croak "$class func not given"; + $self->{logfile} + or croak "$class log file not given"; + open(my $fh, '>', $self->{logfile}) + or die "$class log file $self->{logfile} create failed: $!"; + $fh->autoflush; + $self->{log} = $fh; + return bless $self, $class; +} + +sub run { + my $self = shift; + + pipe(my $reader, my $writer) + or die ref($self), " pipe to child failed: $!"; + defined(my $pid = fork()) + or die ref($self), " fork child failed: $!"; + if ($pid) { + $CHILDREN{$pid} = 1; + $self->{pid} = $pid; + close($reader); + $self->{pipe} = $writer; + return $self; + } + %CHILDREN = (); + $SIG{TERM} = $SIG{INT} = 'DEFAULT'; + $SIG{__DIE__} = sub { + die @_ if $^S; + warn @_; + IO::Handle::flush(\*STDERR); + POSIX::_exit(255); + }; + open(STDERR, '>&', $self->{log}) + or die ref($self), " dup STDERR failed: $!"; + close($writer); + open(STDIN, '<&', $reader) + or die ref($self), " dup STDIN failed: $!"; + close($reader); + + do { + $self->child(); + print STDERR $self->{up}, "\n"; + $self->{begin} = time(); + $self->{func}->($self); + } while ($self->{redo}); + $self->{end} = time(); + print STDERR "Shutdown", "\n"; + if ($self->{timefile}) { + open(my $fh, '>>', $self->{timefile}) + or die ref($self), " open $self->{timefile} failed: $!"; + printf $fh "time='%s' duration='%.10g' ". + "test='%s'\n", + scalar(localtime(time())), $self->{end} - $self->{begin}, + basename($self->{testfile}); + } + + IO::Handle::flush(\*STDOUT); + IO::Handle::flush(\*STDERR); + POSIX::_exit(0); +} + +sub wait { + my $self = shift; + my $flags = shift; + + my $pid = $self->{pid} + or croak ref($self), " no child pid"; + my $kid = waitpid($pid, $flags); + if ($kid > 0) { + my $status = $?; + my $code; + $code = "exit: ". WEXITSTATUS($?) if WIFEXITED($?); + $code = "signal: ". WTERMSIG($?) if WIFSIGNALED($?); + $code = "stop: ". WSTOPSIG($?) if WIFSTOPPED($?); + delete $CHILDREN{$pid} if WIFEXITED($?) || WIFSIGNALED($?); + return wantarray ? ($kid, $status, $code) : $kid; + } + return $kid; +} + +sub loggrep { + my $self = shift; + my($regex, $timeout) = @_; + + my $end = time() + $timeout if $timeout; + + do { + my($kid, $status, $code) = $self->wait(WNOHANG); + if ($kid > 0 && $status != 0 && !$self->{dryrun}) { + # child terminated with failure + die ref($self), " child status: $status $code"; + } + open(my $fh, '<', $self->{logfile}) + or die ref($self), " log file open failed: $!"; + my @match = grep { /$regex/ } <$fh>; + return wantarray ? @match : $match[0] if @match; + close($fh); + # pattern not found + if ($kid == 0) { + # child still running, wait for log data + sleep .1; + } else { + # child terminated, no new log data possible + return; + } + } while ($timeout and time() < $end); + + return; +} + +sub up { + my $self = shift; + my $timeout = shift || 10; + $self->loggrep(qr/$self->{up}/, $timeout) + or croak ref($self), " no '$self->{up}' in $self->{logfile} ". + "after $timeout seconds"; + return $self; +} + +sub down { + my $self = shift; + my $timeout = shift || 300; + $self->loggrep(qr/$self->{down}/, $timeout) + or croak ref($self), " no '$self->{down}' in $self->{logfile} ". + "after $timeout seconds"; + return $self; +} + +sub kill_child { + my $self = shift; + kill_children($self->{pid}); + return $self; +} + +1; diff --git a/regress/tests/README b/regress/tests/README new file mode 100644 index 0000000..c9c7836 --- /dev/null +++ b/regress/tests/README @@ -0,0 +1,20 @@ +Run httpd regressions tests. The framework runs a client and a httpd. +Each test creates a special httpd.conf and starts those two processes. +All processes write log files that are checked for certain messages. +The test arguments are kept in the args-*.pl files. + +SUDO=sudo +As httpd needs root privileges either run the tests as root or set +this variable and run make as a regular user. Only the code that +requires it, is run as root. + +KTRACE=ktrace +Set this variable if you want a ktrace output from httpd. Note that +ktrace is invoked after sudo as sudo would disable it. + +HTTPD=/usr/src/usr.sbin/httpd/obj/httpd +Start an alternative httpd program that is not in the path. + +HTDOCS_SPARSE=no +Set to anything other than "yes" to create real test files instead of +sparse files. This needs more than 1G of free disk space. diff --git a/regress/tests/args-default.pl b/regress/tests/args-default.pl new file mode 100644 index 0000000..3820fdd --- /dev/null +++ b/regress/tests/args-default.pl @@ -0,0 +1,11 @@ +# test default values + +use strict; +use warnings; + +our %args = ( + len => 512, + md5 => path_md5("512") +); + +1; diff --git a/regress/tests/args-get-1048576.pl b/regress/tests/args-get-1048576.pl new file mode 100644 index 0000000..9253aec --- /dev/null +++ b/regress/tests/args-get-1048576.pl @@ -0,0 +1,15 @@ +use strict; +use warnings; + +my $len = 1048576; +our %args = ( + client => { + path => "$len", + len => $len, + http_vers => [ "1.0" ], + }, + len => 1048576, + md5 => path_md5("$len") +); + +1; diff --git a/regress/tests/args-get-1073741824.pl b/regress/tests/args-get-1073741824.pl new file mode 100644 index 0000000..2b4c5f4 --- /dev/null +++ b/regress/tests/args-get-1073741824.pl @@ -0,0 +1,16 @@ +use strict; +use warnings; + +my $len = 1073741824; +my @lengths = ($len, $len); +our %args = ( + client => { + path => "$len", + http_vers => [ "1.0" ], + lengths => \@lengths, + }, + md5 => path_md5("$len"), + lengths => \@lengths, +); + +1; diff --git a/regress/tests/args-get-512.pl b/regress/tests/args-get-512.pl new file mode 100644 index 0000000..20e92c4 --- /dev/null +++ b/regress/tests/args-get-512.pl @@ -0,0 +1,16 @@ +use strict; +use warnings; + +my $len = 512; +my @lengths = ($len, $len, $len); +our %args = ( + client => { + path => "$len", + http_vers => [ "1.0" ], + lengths => \@lengths, + }, + md5 => path_md5("$len"), + lengths => \@lengths, +); + +1; diff --git a/regress/tests/args-get-slash.pl b/regress/tests/args-get-slash.pl new file mode 100644 index 0000000..9406f4b --- /dev/null +++ b/regress/tests/args-get-slash.pl @@ -0,0 +1,20 @@ +use strict; +use warnings; + +our %args = ( + client => { + func => sub { + my $self = shift; + print "GET /\r\n\r\n"; + }, + nocheck => 1 + }, + httpd => { + loggrep => { + qr/"GET \/" 500 0/ => 1, + }, + }, +); + +1; + diff --git a/regress/tests/args-log-user-agent.pl b/regress/tests/args-log-user-agent.pl new file mode 100644 index 0000000..a8dec17 --- /dev/null +++ b/regress/tests/args-log-user-agent.pl @@ -0,0 +1,17 @@ +use strict; +use warnings; + +our %args = ( + client => { + header => { + "User-Agent" => "regress\t\n\nGET / HTTP/1.0\r\n" + } + }, + httpd => { + loggrep => { + qr/\"regress\\t\\n\\nGET \/ HTTP\/1\.0\"/ => 1, + }, + }, +); + +1; diff --git a/regress/tests/args-tls-get-1073741824.pl b/regress/tests/args-tls-get-1073741824.pl new file mode 100644 index 0000000..12af833 --- /dev/null +++ b/regress/tests/args-tls-get-1073741824.pl @@ -0,0 +1,18 @@ +use strict; +use warnings; + +my $len = 1073741824; +our %args = ( + client => { + tls => 1, + path => "$len", + len => $len, + }, + httpd => { + listentls => 1, + }, + len => $len, + md5 => path_md5("$len"), +); + +1; diff --git a/regress/tests/args-tls.pl b/regress/tests/args-tls.pl new file mode 100644 index 0000000..29321a2 --- /dev/null +++ b/regress/tests/args-tls.pl @@ -0,0 +1,18 @@ +# test https connection + +use strict; +use warnings; + +our %args = ( + client => { + tls => 1, + loggrep => 'Issuer.*/OU=ca/', + }, + httpd => { + listentls => 1, + }, + len => 512, + md5 => path_md5("512") +); + +1; diff --git a/regress/tests/funcs.pl b/regress/tests/funcs.pl new file mode 100644 index 0000000..2738d41 --- /dev/null +++ b/regress/tests/funcs.pl @@ -0,0 +1,487 @@ +# $OpenBSD: funcs.pl,v 1.4 2015/07/16 18:50:09 reyk Exp $ + +# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use strict; +use warnings; +no warnings 'experimental::smartmatch'; +use feature 'switch'; +use Errno; +use Digest::MD5; +use Socket; +use Socket6; +use IO::Socket; +use IO::Socket::INET6; + +sub find_ports { + my %args = @_; + my $num = delete $args{num} // 1; + my $domain = delete $args{domain} // AF_INET; + my $addr = delete $args{addr} // "127.0.0.1"; + + my @sockets = (1..$num); + foreach my $s (@sockets) { + $s = IO::Socket::INET6->new( + Proto => "tcp", + Domain => $domain, + $addr ? (LocalAddr => $addr) : (), + ) or die "find_ports: create and bind socket failed: $!"; + } + my @ports = map { $_->sockport() } @sockets; + + return @ports; +} + +sub path_md5 { + my $name = shift; + my $val = `cat md5-$name`; +} + +######################################################################## +# Client funcs +######################################################################## + +sub write_char { + my $self = shift; + my $len = shift // $self->{len} // 512; + my $sleep = $self->{sleep}; + + my $ctx = Digest::MD5->new(); + my $char = '0'; + for (my $i = 1; $i < $len; $i++) { + $ctx->add($char); + print $char + or die ref($self), " print failed: $!"; + given ($char) { + when(/9/) { $char = 'A' } + when(/Z/) { $char = 'a' } + when(/z/) { $char = "\n" } + when(/\n/) { print STDERR "."; $char = '0' } + default { $char++ } + } + if ($self->{sleep}) { + IO::Handle::flush(\*STDOUT); + sleep $self->{sleep}; + } + } + if ($len) { + $char = "\n"; + $ctx->add($char); + print $char + or die ref($self), " print failed: $!"; + print STDERR ".\n"; + } + IO::Handle::flush(\*STDOUT); + + print STDERR "LEN: ", $len, "\n"; + print STDERR "MD5: ", $ctx->hexdigest, "\n"; +} + +sub http_client { + my $self = shift; + + unless ($self->{lengths}) { + # only a single http request + my $len = shift // $self->{len} // 512; + my $cookie = $self->{cookie}; + http_request($self, $len, "1.0", $cookie); + http_response($self, $len); + return; + } + + $self->{http_vers} ||= ["1.1", "1.0"]; + my $vers = $self->{http_vers}[0]; + my @lengths = @{$self->{redo}{lengths} || $self->{lengths}}; + my @cookies = @{$self->{redo}{cookies} || $self->{cookies} || []}; + while (defined (my $len = shift @lengths)) { + my $cookie = shift @cookies || $self->{cookie}; + eval { + http_request($self, $len, $vers, $cookie); + http_response($self, $len); + }; + warn $@ if $@; + if (@lengths && ($@ || $vers eq "1.0")) { + # reconnect and redo the outstanding requests + $self->{redo} = { + lengths => \@lengths, + cookies => \@cookies, + }; + return; + } + } + delete $self->{redo}; + shift @{$self->{http_vers}}; + if (@{$self->{http_vers}}) { + # run the tests again with other persistence + $self->{redo} = { + lengths => [@{$self->{lengths}}], + cookies => [@{$self->{cookies} || []}], + }; + } +} + +sub http_request { + my ($self, $len, $vers, $cookie) = @_; + my $method = $self->{method} || "GET"; + my %header = %{$self->{header} || {}}; + + # encode the requested length or chunks into the url + my $path = ref($len) eq 'ARRAY' ? join("/", @$len) : $len; + # overwrite path with custom path + if (defined($self->{path})) { + $path = $self->{path}; + } + my @request = ("$method /$path HTTP/$vers"); + push @request, "Host: foo.bar" unless defined $header{Host}; + if ($vers eq "1.1" && $method eq "PUT") { + if (ref($len) eq 'ARRAY') { + push @request, "Transfer-Encoding: chunked" + if !defined $header{'Transfer-Encoding'}; + } else { + push @request, "Content-Length: $len" + if !defined $header{'Content-Length'}; + } + } + foreach my $key (sort keys %header) { + my $val = $header{$key}; + if (ref($val) eq 'ARRAY') { + push @request, "$key: $_" + foreach @{$val}; + } else { + push @request, "$key: $val"; + } + } + push @request, "Cookie: $cookie" if $cookie; + push @request, ""; + print STDERR map { ">>> $_\n" } @request; + print map { "$_\r\n" } @request; + if ($method eq "PUT") { + if (ref($len) eq 'ARRAY') { + if ($vers eq "1.1") { + write_chunked($self, @$len); + } else { + write_char($self, $_) foreach (@$len); + } + } else { + write_char($self, $len); + } + } + IO::Handle::flush(\*STDOUT); + # XXX client shutdown seems to be broken in httpd + #shutdown(\*STDOUT, SHUT_WR) + # or die ref($self), " shutdown write failed: $!" + # if $vers ne "1.1"; +} + +sub http_response { + my ($self, $len) = @_; + my $method = $self->{method} || "GET"; + + my $vers; + my $chunked = 0; + { + local $/ = "\r\n"; + local $_ = <STDIN>; + defined + or die ref($self), " missing http $len response"; + chomp; + print STDERR "<<< $_\n"; + m{^HTTP/(\d\.\d) 200 OK$} + or die ref($self), " http response not ok" + unless $self->{httpnok}; + $vers = $1; + while (<STDIN>) { + chomp; + print STDERR "<<< $_\n"; + last if /^$/; + if (/^Content-Length: (.*)/) { + if ($self->{httpnok}) { + $len = $1; + } else { + $1 == $len or die ref($self), + " bad content length $1"; + } + } + if (/^Transfer-Encoding: chunked$/) { + $chunked = 1; + } + } + } + if ($chunked) { + read_chunked($self); + } else { + #$len = $vers eq "1.1" ? $len : undef; + read_char($self, $len) + if $method eq "GET"; + } +} + +sub read_chunked { + my $self = shift; + + for (;;) { + my $len; + { + local $/ = "\r\n"; + local $_ = <STDIN>; + defined or die ref($self), " missing chunk size"; + chomp; + print STDERR "<<< $_\n"; + /^[[:xdigit:]]+$/ + or die ref($self), " chunk size not hex: $_"; + $len = hex; + } + last unless $len > 0; + read_char($self, $len); + { + local $/ = "\r\n"; + local $_ = <STDIN>; + defined or die ref($self), " missing chunk data end"; + chomp; + print STDERR "<<< $_\n"; + /^$/ or die ref($self), " no chunk data end: $_"; + } + } + { + local $/ = "\r\n"; + while (<STDIN>) { + chomp; + print STDERR "<<< $_\n"; + last if /^$/; + } + defined or die ref($self), " missing chunk trailer"; + } +} + +sub errignore { + $SIG{PIPE} = 'IGNORE'; + $SIG{__DIE__} = sub { + die @_ if $^S; + warn "Error ignored"; + warn @_; + IO::Handle::flush(\*STDERR); + POSIX::_exit(0); + }; +} + +######################################################################## +# Server funcs +######################################################################## + +sub read_char { + my $self = shift; + my $max = shift // $self->{max}; + + my $ctx = Digest::MD5->new(); + my $len = 0; + if (defined($max) && $max == 0) { + print STDERR "Max\n"; + } else { + while ((my $r = sysread(STDIN, my $buf, POSIX::BUFSIZ))) { + my $pct; + $_ = $buf; + $len += $r; + $ctx->add($_); + $pct = ($len / $max) * 100.0; + printf(STDERR "%.2f%%\n", $pct); + if (defined($max) && $len >= $max) { + print STDERR "\nMax"; + last; + } + } + print STDERR "\n"; + } + + print STDERR "LEN: ", $len, "\n"; + print STDERR "MD5: ", $ctx->hexdigest, "\n"; +} + +sub http_server { + my $self = shift; + my %header = %{$self->{header} || { Server => "Perl/".$^V }}; + my $cookie = $self->{cookie} || ""; + + my($method, $url, $vers); + do { + my $len; + { + local $/ = "\r\n"; + local $_ = <STDIN>; + return unless defined $_; + chomp; + print STDERR "<<< $_\n"; + ($method, $url, $vers) = m{^(\w+) (.*) HTTP/(1\.[01])$} + or die ref($self), " http request not ok"; + $method =~ /^(GET|PUT)$/ + or die ref($self), " unknown method: $method"; + ($len, my @chunks) = $url =~ /(\d+)/g; + $len = [ $len, @chunks ] if @chunks; + while (<STDIN>) { + chomp; + print STDERR "<<< $_\n"; + last if /^$/; + if ($method eq "PUT" && + /^Content-Length: (.*)/) { + $1 == $len or die ref($self), + " bad content length $1"; + } + $cookie ||= $1 if /^Cookie: (.*)/; + } + } + if ($method eq "PUT" ) { + if (ref($len) eq 'ARRAY') { + read_chunked($self); + } else { + read_char($self, $len); + } + } + + my @response = ("HTTP/$vers 200 OK"); + $len = defined($len) ? $len : scalar(split /|/,$url); + if ($vers eq "1.1" && $method eq "GET") { + if (ref($len) eq 'ARRAY') { + push @response, "Transfer-Encoding: chunked"; + } else { + push @response, "Content-Length: $len"; + } + } + foreach my $key (sort keys %header) { + my $val = $header{$key}; + if (ref($val) eq 'ARRAY') { + push @response, "$key: $_" + foreach @{$val}; + } else { + push @response, "$key: $val"; + } + } + push @response, "Set-Cookie: $cookie" if $cookie; + push @response, ""; + + print STDERR map { ">>> $_\n" } @response; + print map { "$_\r\n" } @response; + + if ($method eq "GET") { + if (ref($len) eq 'ARRAY') { + if ($vers eq "1.1") { + write_chunked($self, @$len); + } else { + write_char($self, $_) foreach (@$len); + } + } else { + write_char($self, $len); + } + } + IO::Handle::flush(\*STDOUT); + } while ($vers eq "1.1"); + $self->{redo}-- if $self->{redo}; +} + +sub write_chunked { + my $self = shift; + my @chunks = @_; + + foreach my $len (@chunks) { + printf STDERR ">>> %x\n", $len; + printf "%x\r\n", $len; + write_char($self, $len); + printf STDERR ">>> \n"; + print "\r\n"; + } + my @trailer = ("0", "X-Chunk-Trailer: @chunks", ""); + print STDERR map { ">>> $_\n" } @trailer; + print map { "$_\r\n" } @trailer; +} + +######################################################################## +# Script funcs +######################################################################## + +sub check_logs { + my ($c, $r, %args) = @_; + + return if $args{nocheck}; + + check_len($c, $r, %args); + check_md5($c, $r, %args); + check_loggrep($c, $r, %args); + $r->loggrep("lost child") + and die "httpd lost child"; +} + +sub check_len { + my ($c, $r, %args) = @_; + + $args{len} ||= 512 unless $args{lengths}; + + my @clen = $c->loggrep(qr/^LEN: /) or die "no client len" + unless $args{client}{nocheck}; +# !@clen +# or die "client: @clen", "len mismatch"; + !defined($args{len}) || !$clen[0] || $clen[0] eq "LEN: $args{len}\n" + or die "client: $clen[0]", "len $args{len} expected"; + my @lengths = map { ref eq 'ARRAY' ? @$_ : $_ } + @{$args{lengths} || []}; + foreach my $len (@lengths) { + unless ($args{client}{nocheck}) { + my $clen = shift @clen; + $clen eq "LEN: $len\n" + or die "client: $clen", "len $len expected"; + } + } +} + +sub check_md5 { + my ($c, $r, %args) = @_; + + my @cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck}; + my @md5 = ref($args{md5}) eq 'ARRAY' ? @{$args{md5}} : $args{md5} || () + or return; + foreach my $md5 (@md5) { + unless ($args{client}{nocheck}) { + my $cmd5 = shift @cmd5 + or die "too few md5 in client log"; + $cmd5 =~ /^MD5: ($md5)$/ + or die "client: $cmd5", "md5 $md5 expected"; + } + } + @cmd5 && ref($args{md5}) eq 'ARRAY' + and die "too many md5 in client log"; +} + +sub check_loggrep { + my ($c, $r, %args) = @_; + + my %name2proc = (client => $c, httpd => $r); + foreach my $name (qw(client httpd)) { + my $p = $name2proc{$name} or next; + my $pattern = $args{$name}{loggrep} or next; + $pattern = [ $pattern ] unless ref($pattern) eq 'ARRAY'; + foreach my $pat (@$pattern) { + if (ref($pat) eq 'HASH') { + while (my($re, $num) = each %$pat) { + my @matches = $p->loggrep($re); + @matches == $num + or die "$name matches '@matches': ", + "'$re' => $num"; + } + } else { + $p->loggrep($pat) + or die "$name log missing pattern: '$pat'"; + } + } + } +} + +1; diff --git a/regress/tests/httpd.pl b/regress/tests/httpd.pl new file mode 100644 index 0000000..481a587 --- /dev/null +++ b/regress/tests/httpd.pl @@ -0,0 +1,73 @@ +#!/usr/bin/perl +# $OpenBSD: httpd.pl,v 1.1 2015/07/16 16:35:57 reyk Exp $ + +# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org> +# Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org> +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use strict; +use warnings; +use Socket; +use Socket6; + +use Client; +use Httpd; +require 'funcs.pl'; + +sub usage { + die "usage: httpd.pl chroot [test-args.pl]\n"; +} + +my $testfile; +our %args; +if (@ARGV and -f $ARGV[-1]) { + $testfile = pop; + do $testfile + or die "Do test file $testfile failed: ", $@ || $!; +} +@ARGV == 1 or usage(); + +my $redo = $args{lengths} && @{$args{lengths}}; +$redo = 0 if $args{client}{http_vers}; # run only one persistent connection +my($sport, $rport) = find_ports(num => 2); +my $d = Httpd->new( + chroot => $ARGV[0], + listendomain => AF_INET, + listenaddr => "127.0.0.1", + listenport => $rport, + connectdomain => AF_INET, + connectaddr => "127.0.0.1", + connectport => $sport, + %{$args{httpd}}, + testfile => $testfile, +); +my $c = Client->new( + chroot => $ARGV[0], + func => \&http_client, + connectdomain => AF_INET, + connectaddr => "127.0.0.1", + connectport => $rport, + %{$args{client}}, + testfile => $testfile, +) unless $args{client}{noclient}; + +$d->run; +$d->up; +$c->run->up unless $args{client}{noclient}; + +$c->down unless $args{client}{noclient}; +$d->kill_child; +$d->down; + +check_logs($c, $d, %args); |