commit 8aa213c123cbcfc2e50a77142c2206c202705e90
parent f45ca668af3d615e9215db49f190ea2833cecc18
Author: Laslo Hunhold <dev@frign.de>
Date:   Wed,  3 Feb 2021 18:33:51 +0100
Refactor resource-parsing and -handling heavily
The previous approach of stripping query and fragment from the resource
was flawed and fixing this issue motivated a much deeper refactor that
was on my todo-list for a while.
First off, the resource-request (e.g. "/projects/index.html?123#eme")
is made up of the path ("/projects/index.html") and optionally the
query ("123") and fragment ("eme"). Instead of trying to break it up
or stripping it in the response-generation, we now store these three
things separately in the request-struct. Calling the resource-request
an "URI" was wrong, as the URI also includes the authority (i.e. the
host in this case) and the protocol-prefix (i.e. "http://"). To fix
this, the respective fields in the request- and response-structs had
to be renamed, as follows:
This commit adds a differentiation between a "path" (what is publicly
requested) and an "internal_path" (what we actually serve in the file
system). These two things can differ, e.g. with virtual hosts. The
cleanup and generation of these paths for the response-struct is heavily
refactored in http_prepare_response(), eliminating some deep bugs that
were due to the previously complicated approach.
Instead of doing everything by hand and having a very complicated logic
on when, after the path-cleanup, it was necessary to do a redirect, the
cleanup sections are exported to separate functions which indicate when
a redirect is necessary. This also makes more complex path-processing
possible, if desired, and definitely increases the readability.
In total, this makes the path-processing much more straightforward, and
fixes the problem where a redirect would strip queries and fragments.
Possible file-access problems for virtual hosts were also fixed while
also eliminating the pretty hacky RELPATH-macro, among other things.
Signed-off-by: Laslo Hunhold <dev@frign.de>
Diffstat:
| M | data.c | | | 6 | +++--- | 
| M | http.c | | | 447 | +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ | 
| M | http.h | | | 7 | +++++-- | 
| M | main.c | | | 5 | +++-- | 
4 files changed, 290 insertions(+), 175 deletions(-)
diff --git a/data.c b/data.c
@@ -104,13 +104,13 @@ data_prepare_dirlisting_buf(const struct response *res,
 	memset(buf, 0, sizeof(*buf));
 
 	/* read directory */
-	if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) {
+	if ((dirlen = scandir(res->internal_path, &e, NULL, compareent)) < 0) {
 		return S_FORBIDDEN;
 	}
 
 	if (*progress == 0) {
 		/* write listing header (sizeof(esc) >= PATH_MAX) */
-		html_escape(res->uri, esc, MIN(PATH_MAX, sizeof(esc)));
+		html_escape(res->path, esc, MIN(PATH_MAX, sizeof(esc)));
 		if (buffer_appendf(buf,
 		                   "<!DOCTYPE html>\n<html>\n\t<head>"
 		                   "<title>Index of %s</title></head>\n"
@@ -197,7 +197,7 @@ data_prepare_file_buf(const struct response *res, struct buffer *buf,
 	memset(buf, 0, sizeof(*buf));
 
 	/* open file */
-	if (!(fp = fopen(res->path, "r"))) {
+	if (!(fp = fopen(res->internal_path, "r"))) {
 		s = S_FORBIDDEN;
 		goto cleanup;
 	}
diff --git a/http.c b/http.c
@@ -205,7 +205,7 @@ http_parse_header(const char *h, struct request *req)
 {
 	struct in6_addr addr;
 	size_t i, mlen;
-	const char *p, *q;
+	const char *p, *q, *r, *s, *t;
 	char *m, *n;
 
 	/* empty the request struct */
@@ -235,16 +235,94 @@ http_parse_header(const char *h, struct request *req)
 	/* basis for next step */
 	p = h + mlen + 1;
 
-	/* TARGET */
+	/* RESOURCE */
+
+	/*
+	 * path?query#fragment
+	 * ^   ^     ^        ^
+	 * |   |     |        |
+	 * p   r     s        q
+	 *
+	 */
 	if (!(q = strchr(p, ' '))) {
 		return S_BAD_REQUEST;
 	}
-	if (q - p + 1 > PATH_MAX) {
+
+	/* search for first '?' */
+	for (r = p; r < q; r++) {
+		if (!isprint(*r)) {
+			return S_BAD_REQUEST;
+		}
+		if (*r == '?') {
+			break;
+		}
+	}
+	if (r == q) {
+		/* not found */
+		r = NULL;
+	}
+
+	/* search for first '#' */
+	for (s = p; s < q; s++) {
+		if (!isprint(*s)) {
+			return S_BAD_REQUEST;
+		}
+		if (*s == '#') {
+			break;
+		}
+	}
+	if (s == q) {
+		/* not found */
+		s = NULL;
+	}
+
+	if (r != NULL && s != NULL && s < r) {
+		/*
+		 * '#' comes before '?' and thus the '?' is literal,
+		 * because the query must come before the fragment
+		 */
+		r = NULL;
+	}
+
+	/* write path using temporary endpointer t */
+	if (r != NULL) {
+		/* resource contains a query, path ends at r */
+		t = r;
+	} else if (s != NULL) {
+		/* resource contains only a fragment, path ends at s */
+		t = s;
+	} else {
+		/* resource contains no queries, path ends at q */
+		t = q;
+	}
+	if ((size_t)(t - p + 1) > LEN(req->path)) {
 		return S_REQUEST_TOO_LARGE;
 	}
-	memcpy(req->uri, p, q - p);
-	req->uri[q - p] = '\0';
-	decode(req->uri, req->uri);
+	memcpy(req->path, p, t - p);
+	req->path[t - p] = '\0';
+	decode(req->path, req->path);
+
+	/* write query if present */
+	if (r != NULL) {
+		/* query ends either at s (if fragment present) or q */
+		t = (s != NULL) ? s : q;
+
+		if ((size_t)(t - (r + 1) + 1) > LEN(req->query)) {
+			return S_REQUEST_TOO_LARGE;
+		}
+		memcpy(req->query, r + 1, t - (r + 1));
+		req->query[t - (r + 1)] = '\0';
+	}
+
+	/* write fragment if present */
+	if (s != NULL) {
+		/* the fragment always starts at s + 1 and ends at q */
+		if ((size_t)(q - (s + 1) + 1) > LEN(req->fragment)) {
+			return S_REQUEST_TOO_LARGE;
+		}
+		memcpy(req->fragment, s + 1, q - (s + 1));
+		req->fragment[q - (s + 1)] = '\0';
+	}
 
 	/* basis for next step */
 	p = q + 1;
@@ -304,7 +382,7 @@ http_parse_header(const char *h, struct request *req)
 		if (!(q = strstr(p, "\r\n"))) {
 			return S_BAD_REQUEST;
 		}
-		if (q - p + 1 > FIELD_MAX) {
+		if ((size_t)(q - p + 1) > LEN(req->field[i])) {
 			return S_REQUEST_TOO_LARGE;
 		}
 		memcpy(req->field[i], p, q - p);
@@ -372,24 +450,24 @@ encode(const char src[PATH_MAX], char dest[PATH_MAX])
 	dest[i] = '\0';
 }
 
-static int
-normabspath(char *path)
+static enum status
+path_normalize(char *uri, int *redirect)
 {
 	size_t len;
-	int dirty = 0, last = 0;
+	int last = 0;
 	char *p, *q;
 
 	/* require and skip first slash */
-	if (path[0] != '/') {
-		return -1;
+	if (uri[0] != '/') {
+		return S_BAD_REQUEST;
 	}
-	p = path + 1;
+	p = uri + 1;
 
-	/* get length of path */
+	/* get length of URI */
 	len = strlen(p);
 
 	for (; !last; ) {
-		/* bound path component within (p,q) */
+		/* bound uri component within (p,q) */
 		if (!(q = strchr(p, '/'))) {
 			q = strchr(p, '\0');
 			last = 1;
@@ -402,9 +480,9 @@ normabspath(char *path)
 			goto squash;
 		} else if (q - p == 2 && p[0] == '.' && p[1] == '.') {
 			/* "../" */
-			if (p != path + 1) {
+			if (p != uri + 1) {
 				/* place p right after the previous / */
-				for (p -= 2; p > path && *p != '/'; p--);
+				for (p -= 2; p > uri && *p != '/'; p--);
 				p++;
 			}
 			goto squash;
@@ -417,15 +495,90 @@ squash:
 		/* squash (p,q) into void */
 		if (last) {
 			*p = '\0';
-			len = p - path;
+			len = p - uri;
 		} else {
-			memmove(p, q + 1, len - ((q + 1) - path) + 2);
+			memmove(p, q + 1, len - ((q + 1) - uri) + 2);
 			len -= (q + 1) - p;
 		}
-		dirty = 1;
+		if (redirect != NULL) {
+			*redirect = 1;
+		}
+	}
+
+	return 0;
+}
+
+static enum status
+path_add_vhost_prefix(char uri[PATH_MAX], int *redirect,
+                     const struct server *srv, const struct response *res)
+{
+	if (srv->vhost && res->vhost && res->vhost->prefix) {
+		if (prepend(uri, PATH_MAX, res->vhost->prefix)) {
+			return S_REQUEST_TOO_LARGE;
+		}
+		if (redirect != NULL) {
+			*redirect = 1;
+		}
+	}
+
+	return 0;
+}
+
+static enum status
+path_apply_prefix_mapping(char uri[PATH_MAX], int *redirect,
+                         const struct server *srv, const struct response *res)
+{
+	size_t i, len;
+
+	for (i = 0; i < srv->map_len; i++) {
+		len = strlen(srv->map[i].from);
+		if (!strncmp(uri, srv->map[i].from, len)) {
+			/*
+			 * if vhosts are enabled only apply mappings
+			 * defined for the current canonical host
+			 */
+			if (srv->vhost && res->vhost && srv->map[i].chost &&
+			    strcmp(srv->map[i].chost, res->vhost->chost)) {
+				continue;
+			}
+
+			/* swap out URI prefix */
+			memmove(uri, uri + len, strlen(uri) + 1);
+			if (prepend(uri, PATH_MAX, srv->map[i].to)) {
+				return S_REQUEST_TOO_LARGE;
+			}
+
+			if (redirect != NULL) {
+				*redirect = 1;
+			}
+
+			/* break so we don't possibly hit an infinite loop */
+			break;
+		}
+	}
+
+	return 0;
+}
+
+static enum status
+path_ensure_dirslash(char uri[PATH_MAX], int *redirect)
+{
+	size_t len;
+
+	/* append '/' to URI if not present */
+	len = strlen(uri);
+	if (len + 1 + 1 > PATH_MAX) {
+		return S_REQUEST_TOO_LARGE;
+	}
+	if (len > 0 && uri[len - 1] != '/') {
+		uri[len] = '/';
+		uri[len + 1] = '\0';
+		if (redirect != NULL) {
+			*redirect = 1;
+		}
 	}
 
-	return dirty;
+	return 0;
 }
 
 static enum status
@@ -560,60 +713,29 @@ parse_range(const char *str, size_t size, size_t *lower, size_t *upper)
 	return 0;
 }
 
-#undef RELPATH
-#define RELPATH(x) ((!*(x) || !strcmp(x, "/")) ? "." : ((x) + 1))
-
 void
 http_prepare_response(const struct request *req, struct response *res,
                       const struct server *srv)
 {
-	enum status s;
+	enum status s, tmps;
 	struct in6_addr addr;
 	struct stat st;
 	struct tm tm = { 0 };
-	struct vhost *vhost;
-	size_t len, i;
-	int dirty = 0, hasport, ipv6host;
-	static char realuri[PATH_MAX], tmpuri[PATH_MAX];
+	size_t i;
+	int redirect, hasport, ipv6host;
+	static char tmppath[PATH_MAX];
 	char *p, *mime;
-	const char *targethost;
 
 	/* empty all response fields */
 	memset(res, 0, sizeof(*res));
 
-	/*
-	 * make a working copy of the URI, strip queries and fragments
-	 * (ignorable according to RFC 3986 section 3) and normalize it
-	 */
-	memcpy(realuri, req->uri, sizeof(realuri));
-
-	if ((p = strchr(realuri, '?'))) {
-		*p = '\0';
-	} else if ((p = strchr(realuri, '#'))) {
-		*p = '\0';
-	}
-
-	switch (normabspath(realuri)) {
-	case -1:
-		s = S_BAD_REQUEST;
-		goto err;
-	case 0:
-		/* string is unchanged */
-		break;
-	case 1:
-		/* string was changed */
-		dirty = 1;
-		break;
-	}
-
-	/* match vhost */
-	vhost = NULL;
+	/* determine virtual host */
 	if (srv->vhost) {
 		for (i = 0; i < srv->vhost_len; i++) {
-			if (!regexec(&(srv->vhost[i].re), req->field[REQ_HOST],
-			             0, NULL, 0)) {
+			if (!regexec(&(srv->vhost[i].re),
+			             req->field[REQ_HOST], 0, NULL, 0)) {
 				/* we have a matching vhost */
-				vhost = &(srv->vhost[i]);
+				res->vhost = &(srv->vhost[i]);
 				break;
 			}
 		}
@@ -621,100 +743,74 @@ http_prepare_response(const struct request *req, struct response *res,
 			s = S_NOT_FOUND;
 			goto err;
 		}
-
-		/* if we have a vhost prefix, prepend it to the URI */
-		if (vhost->prefix) {
-			if (prepend(realuri, LEN(realuri), vhost->prefix)) {
-				s = S_REQUEST_TOO_LARGE;
-				goto err;
-			}
-			dirty = 1;
-		}
 	}
 
-	/* apply URI prefix mapping */
-	for (i = 0; i < srv->map_len; i++) {
-		len = strlen(srv->map[i].from);
-		if (!strncmp(realuri, srv->map[i].from, len)) {
-			/* match canonical host if vhosts are enabled and
-			 * the mapping specifies a canonical host */
-			if (srv->vhost && srv->map[i].chost &&
-			    strcmp(srv->map[i].chost, vhost->chost)) {
-				continue;
-			}
+	/* copy request-path to response-path and clean it up */
+	redirect = 0;
+	memcpy(res->path, req->path, MIN(sizeof(res->path), sizeof(req->path)));
+	if ((tmps = path_normalize(res->path, &redirect)) ||
+	    (tmps = path_add_vhost_prefix(res->path, &redirect, srv, res)) ||
+	    (tmps = path_apply_prefix_mapping(res->path, &redirect, srv, res)) ||
+	    (tmps = path_normalize(res->path, &redirect))) {
+		s = tmps;
+		goto err;
+	}
 
-			/* swap out URI prefix */
-			memmove(realuri, realuri + len, strlen(realuri) + 1);
-			if (prepend(realuri, LEN(realuri), srv->map[i].to)) {
-				s = S_REQUEST_TOO_LARGE;
-				goto err;
-			}
-			dirty = 1;
-			break;
-		}
+	/* redirect all non-canonical hosts to their canonical forms */
+	if (srv->vhost && res->vhost &&
+	    strcmp(req->field[REQ_HOST], res->vhost->chost)) {
+		redirect = 1;
 	}
 
-	/* normalize URI again, in case we introduced dirt */
-	switch (normabspath(realuri)) {
-	case -1:
-		s = S_BAD_REQUEST;
+	/* reject all non-well-known hidden targets (see RFC 8615) */
+	if (strstr(res->path, "/.") && strncmp(res->path, "/.well-known/",
+	                                  sizeof("/.well-known/") - 1)) {
+		s = S_FORBIDDEN;
 		goto err;
-	case 0:
-		/* string is unchanged */
-		break;
-	case 1:
-		/* string was changed */
-		dirty = 1;
-		break;
 	}
 
-	/* stat the relative path derived from the URI */
-	if (stat(RELPATH(realuri), &st) < 0) {
+	/*
+	 * generate and stat internal path based on the cleaned up request
+	 * path and the virtual host while ignoring query and fragment
+	 * (valid according to RFC 3986)
+	 */
+	if (esnprintf(res->internal_path, sizeof(res->internal_path), "/%s/%s",
+	              (srv->vhost && res->vhost) ? res->vhost->dir : "",
+		      res->path)) {
+		s = S_REQUEST_TOO_LARGE;
+		goto err;
+	}
+	if ((tmps = path_normalize(res->internal_path, NULL))) {
+		s = tmps;
+		goto err;
+	}
+	if (stat(res->internal_path, &st) < 0) {
 		s = (errno == EACCES) ? S_FORBIDDEN : S_NOT_FOUND;
 		goto err;
 	}
 
+	/*
+	 * if the path points at a directory, make sure both the path
+	 * and internal path have a trailing slash
+	 */
 	if (S_ISDIR(st.st_mode)) {
-		/* append '/' to URI if not present */
-		len = strlen(realuri);
-		if (len + 1 + 1 > PATH_MAX) {
-			s = S_REQUEST_TOO_LARGE;
+		if ((tmps = path_ensure_dirslash(res->path, &redirect)) ||
+		    (tmps = path_ensure_dirslash(res->internal_path, NULL))) {
+			s = tmps;
 			goto err;
 		}
-		if (len > 0 && realuri[len - 1] != '/') {
-			realuri[len] = '/';
-			realuri[len + 1] = '\0';
-			dirty = 1;
-		}
 	}
 
-	/*
-	 * reject hidden targets, except if it is a well-known URI
-	 * according to RFC 8615
-	 */
-	if (strstr(realuri, "/.") && strncmp(realuri,
-	    "/.well-known/", sizeof("/.well-known/") - 1)) {
-		s = S_FORBIDDEN;
-		goto err;
-	}
-
-	/*
-	 * redirect if the URI needs to be redirected or the requested
-	 * host is non-canonical
-	 */
-	if (dirty || (srv->vhost && vhost &&
-	    strcmp(req->field[REQ_HOST], vhost->chost))) {
+	/* redirect if the path-cleanup necessitated it earlier */
+	if (redirect) {
 		res->status = S_MOVED_PERMANENTLY;
 
-		/* encode realuri */
-		encode(realuri, tmpuri);
+		/* encode path */
+		encode(res->path, tmppath);
 
 		/* determine target location */
-		if (srv->vhost) {
+		if (srv->vhost && res->vhost) {
 			/* absolute redirection URL */
-			targethost = req->field[REQ_HOST][0] ? vhost->chost ?
-			             vhost->chost : req->field[REQ_HOST] :
-				     srv->host ? srv->host : "localhost";
 
 			/* do we need to add a port to the Location? */
 			hasport = srv->port && strcmp(srv->port, "80");
@@ -722,70 +818,83 @@ http_prepare_response(const struct request *req, struct response *res,
 			/* RFC 2732 specifies to use brackets for IPv6-addresses
 			 * in URLs, so we need to check if our host is one and
 			 * honor that later when we fill the "Location"-field */
-			if ((ipv6host = inet_pton(AF_INET6, targethost,
+			if ((ipv6host = inet_pton(AF_INET6, res->vhost->chost,
 			                          &addr)) < 0) {
 				s = S_INTERNAL_SERVER_ERROR;
 				goto err;
 			}
 
-			/* write location to response struct */
+			/*
+			 * write location to response struct (re-including
+			 * the query and fragment, if present)
+			 */
 			if (esnprintf(res->field[RES_LOCATION],
 			              sizeof(res->field[RES_LOCATION]),
-			              "//%s%s%s%s%s%s",
+			              "//%s%s%s%s%s%s%s%s%s%s",
 			              ipv6host ? "[" : "",
-			              targethost,
-			              ipv6host ? "]" : "", hasport ? ":" : "",
-			              hasport ? srv->port : "", tmpuri)) {
+			              res->vhost->chost,
+			              ipv6host ? "]" : "",
+				      hasport ? ":" : "",
+			              hasport ? srv->port : "",
+				      tmppath,
+				      req->query[0] ? "?" : "",
+				      req->query,
+				      req->fragment[0] ? "#" : "",
+				      req->fragment)) {
 				s = S_REQUEST_TOO_LARGE;
 				goto err;
 			}
 		} else {
-			/* write relative redirection URI to response struct */
+			/*
+			 * write relative redirection URI to response struct
+			 * (re-including the query and fragment, if present)
+			 */
 			if (esnprintf(res->field[RES_LOCATION],
 			              sizeof(res->field[RES_LOCATION]),
-			              "%s", tmpuri)) {
+			              "%s%s%s%s%s",
+				      tmppath,
+				      req->query[0] ? "?" : "",
+				      req->query,
+				      req->fragment[0] ? "#" : "",
+				      req->fragment)) {
 				s = S_REQUEST_TOO_LARGE;
 				goto err;
 			}
 		}
 
 		return;
-	} else {
-		/*
-		 * the URI is well-formed, we can now write the URI into
-		 * the response-URI and corresponding relative path
-		 * (optionally including the vhost servedir as a prefix)
-		 * into the actual response-path
-		 */
-		if (esnprintf(res->uri, sizeof(res->uri), "%s", realuri)) {
-			s = S_REQUEST_TOO_LARGE;
-			goto err;
-		}
-		if (esnprintf(res->path, sizeof(res->path), "%s%s",
-		    vhost ? vhost->dir : "", RELPATH(realuri))) {
-			s = S_REQUEST_TOO_LARGE;
-			goto err;
-		}
 	}
 
 	if (S_ISDIR(st.st_mode)) {
 		/*
-		 * check if the directory index exists by appending it to
-		 * the URI
+		 * when we serve a directory, we first check if there
+		 * exists a directory index. If not, we either make
+		 * a directory listing (if enabled) or send an error
+		 */
+
+		/*
+		 * append docindex to internal_path temporarily
+		 * (internal_path is guaranteed to end with '/')
 		 */
-		if (esnprintf(tmpuri, sizeof(tmpuri), "%s%s",
-		              realuri, srv->docindex)) {
+		if (esnprintf(tmppath, sizeof(tmppath), "%s%s",
+		              res->internal_path, srv->docindex)) {
 			s = S_REQUEST_TOO_LARGE;
 			goto err;
 		}
 
-		/* stat the docindex, which must be a regular file */
-		if (stat(RELPATH(tmpuri), &st) < 0 || !S_ISREG(st.st_mode)) {
+		/* stat the temporary path, which must be a regular file */
+		if (stat(tmppath, &st) < 0 || !S_ISREG(st.st_mode)) {
 			if (srv->listdirs) {
 				/* serve directory listing */
+
+				/* check if directory is accessible */
+				if (access(res->internal_path, R_OK) != 0) {
+					s = S_FORBIDDEN;
+					goto err;
+				} else {
+					res->status = S_OK;
+				}
 				res->type = RESTYPE_DIRLISTING;
-				res->status = (access(res->path, R_OK)) ?
-				              S_FORBIDDEN : S_OK;
 
 				if (esnprintf(res->field[RES_CONTENT_TYPE],
 				              sizeof(res->field[RES_CONTENT_TYPE]),
@@ -802,8 +911,10 @@ http_prepare_response(const struct request *req, struct response *res,
 				goto err;
 			}
 		} else {
-			/* docindex is valid, write tmpuri to response-path */
-			if (esnprintf(res->path, sizeof(res->path), "%s", tmpuri)) {
+			/* the docindex exists; copy tmppath to internal path */
+			if (esnprintf(res->internal_path,
+			              sizeof(res->internal_path), "%s",
+			              tmppath)) {
 				s = S_REQUEST_TOO_LARGE;
 				goto err;
 			}
@@ -847,7 +958,7 @@ http_prepare_response(const struct request *req, struct response *res,
 
 	/* mime */
 	mime = "application/octet-stream";
-	if ((p = strrchr(res->path, '.'))) {
+	if ((p = strrchr(res->internal_path, '.'))) {
 		for (i = 0; i < LEN(mimes); i++) {
 			if (!strcmp(mimes[i].ext, p + 1)) {
 				mime = mimes[i].type;
@@ -860,7 +971,7 @@ http_prepare_response(const struct request *req, struct response *res,
 	res->type = RESTYPE_FILE;
 
 	/* check if file is readable */
-	res->status = (access(res->path, R_OK)) ? S_FORBIDDEN :
+	res->status = (access(res->internal_path, R_OK)) ? S_FORBIDDEN :
 	              (req->field[REQ_RANGE][0] != '\0') ?
 	              S_PARTIAL_CONTENT : S_OK;
 
diff --git a/http.h b/http.h
@@ -27,7 +27,9 @@ extern const char *req_method_str[];
 
 struct request {
 	enum req_method method;
-	char uri[PATH_MAX];
+	char path[PATH_MAX];
+	char query[FIELD_MAX];
+	char fragment[FIELD_MAX];
 	char field[NUM_REQ_FIELDS][FIELD_MAX];
 };
 
@@ -73,8 +75,9 @@ struct response {
 	enum res_type type;
 	enum status status;
 	char field[NUM_RES_FIELDS][FIELD_MAX];
-	char uri[PATH_MAX];
 	char path[PATH_MAX];
+	char internal_path[PATH_MAX];
+	struct vhost *vhost;
 	struct {
 		size_t lower;
 		size_t upper;
diff --git a/main.c b/main.c
@@ -45,8 +45,9 @@ logmsg(const struct connection *c)
 		inaddr_str[0] = '\0';
 	}
 
-	printf("%s\t%s\t%d\t%s\t%s\n", tstmp, inaddr_str, c->res.status,
-	       c->req.field[REQ_HOST], c->req.uri);
+	printf("%s\t%s\t%d\t%s\t%s%s%s%s%s\n", tstmp, inaddr_str, c->res.status,
+	       c->req.field[REQ_HOST], c->req.path, c->req.query[0] ? "?" : "",
+	       c->req.query, c->req.fragment[0] ? "#" : "", c->req.fragment);
 }
 
 static void