Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files. =================================================================== RCS file: /ftp/cvs/cvsroot/src/libexec/httpd/bozohttpd.c,v rcsdiff: /ftp/cvs/cvsroot/src/libexec/httpd/bozohttpd.c,v: warning: Unknown phrases like `commitid ...;' are present. retrieving revision 1.28 retrieving revision 1.28.2.3 diff -u -p -r1.28 -r1.28.2.3 --- src/libexec/httpd/bozohttpd.c 2011/08/27 15:33:59 1.28 +++ src/libexec/httpd/bozohttpd.c 2014/05/22 11:37:13 1.28.2.3 @@ -1,9 +1,9 @@ -/* $NetBSD: bozohttpd.c,v 1.28 2011/08/27 15:33:59 joerg Exp $ */ +/* $NetBSD: bozohttpd.c,v 1.28.2.3 2014/05/22 11:37:13 yamt Exp $ */ -/* $eterna: bozohttpd.c,v 1.176 2010/09/20 22:26:28 mrg Exp $ */ +/* $eterna: bozohttpd.c,v 1.178 2011/11/18 09:21:15 mrg Exp $ */ /* - * Copyright (c) 1997-2010 Matthew R. Green + * Copyright (c) 1997-2014 Matthew R. Green * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -55,17 +55,17 @@ /* * requirements for minimal http/1.1 (at least, as documented in - * which expired may 18, 1999): + * RFC 2616 (HTTP/1.1): * - * - 14.15: content-encoding handling. [1] + * - 14.11: content-encoding handling. [1] * - * - 14.16: content-length handling. this is only a SHOULD header + * - 14.13: content-length handling. this is only a SHOULD header * thus we could just not send it ever. [1] * * - 14.17: content-type handling. [1] * - * - 14.25/28: if-{,un}modified-since handling. maybe do this, but - * i really don't want to have to parse 3 differnet date formats + * - 14.28: if-unmodified-since handling. if-modified-since is + * done since, shouldn't be too hard for this one. * * [1] need to revisit to ensure proper behaviour * @@ -88,28 +88,28 @@ * * - 10.3.3/10.3.4/10.3.8: just use '302' codes always. * - * - 14.1/14.2/14.3/14.27: we do not support Accept: headers.. + * - 14.1/14.2/14.3/14.27: we do not support Accept: headers. * just ignore them and send the request anyway. they are * only SHOULD. * - * - 14.5/14.16/14.35: we don't do ranges. from section 14.35.2 - * `A server MAY ignore the Range header'. but it might be nice. - * since 20080301 we support simple range headers. + * - 14.5/14.16/14.35: only support simple ranges: %d- and %d-%d + * would be nice to support more. * * - 14.9: we aren't a cache. * - * - 14.15: content-md5 would be nice... - * - * - 14.24/14.26/14.27: be nice to support this... + * - 14.15: content-md5 would be nice. * - * - 14.44: not sure about this Vary: header. ignore it for now. + * - 14.24/14.26/14.27: if-match, if-none-match, if-range. be + * nice to support this. + * + * - 14.44: Vary: seems unneeded. ignore it for now. */ #ifndef INDEX_HTML #define INDEX_HTML "index.html" #endif #ifndef SERVER_SOFTWARE -#define SERVER_SOFTWARE "bozohttpd/20100920" +#define SERVER_SOFTWARE "bozohttpd/20140201" #endif #ifndef DIRECT_ACCESS_FILE #define DIRECT_ACCESS_FILE ".bzdirect" @@ -337,21 +337,21 @@ bozo_clean_request(bozo_httpreq_t *reque MF(hr_remotehost); MF(hr_remoteaddr); MF(hr_serverport); + MF(hr_virthostname); MF(hr_file); MF(hr_oldfile); MF(hr_query); + MF(hr_host); #undef MF bozo_auth_cleanup(request); for (hdr = SIMPLEQ_FIRST(&request->hr_headers); hdr; hdr = SIMPLEQ_NEXT(hdr, h_next)) { free(hdr->h_value); free(hdr->h_header); - if (ohdr) - free(ohdr); + free(ohdr); ohdr = hdr; } - if (ohdr) - free(ohdr); + free(ohdr); free(request); } @@ -539,6 +539,7 @@ bozo_read_request(bozohttpd_t *httpd) request->hr_range = NULL; request->hr_last_byte_pos = -1; request->hr_if_modified_since = NULL; + request->hr_virthostname = NULL; request->hr_file = NULL; request->hr_oldfile = NULL; @@ -563,14 +564,26 @@ bozo_read_request(bozohttpd_t *httpd) if (addr != NULL) request->hr_remoteaddr = bozostrdup(request->hr_httpd, addr); slen = sizeof(ss); - if (getsockname(0, (struct sockaddr *)(void *)&ss, &slen) < 0) - port = NULL; - else { - if (getnameinfo((struct sockaddr *)(void *)&ss, slen, NULL, 0, - bufport, sizeof bufport, NI_NUMERICSERV) == 0) - port = bufport; + + /* + * Override the bound port from the request value, so it works even + * if passed through a proxy that doesn't rewrite the port. + */ + if (httpd->bindport) { + if (strcmp(httpd->bindport, "80") != 0) + port = httpd->bindport; else port = NULL; + } else { + if (getsockname(0, (struct sockaddr *)(void *)&ss, &slen) < 0) + port = NULL; + else { + if (getnameinfo((struct sockaddr *)(void *)&ss, slen, NULL, 0, + bufport, sizeof bufport, NI_NUMERICSERV) == 0) + port = bufport; + else + port = NULL; + } } if (port != NULL) request->hr_serverport = bozostrdup(request->hr_httpd, port); @@ -669,8 +682,8 @@ bozo_read_request(bozohttpd_t *httpd) else if (strcasecmp(hdr->h_header, "content-length") == 0) request->hr_content_length = hdr->h_value; else if (strcasecmp(hdr->h_header, "host") == 0) - request->hr_host = hdr->h_value; - /* HTTP/1.1 rev06 draft spec: 14.20 */ + request->hr_host = bozostrdup(httpd, hdr->h_value); + /* RFC 2616 (HTTP/1.1): 14.20 */ else if (strcasecmp(hdr->h_header, "expect") == 0) { (void)bozo_http_error(httpd, 417, request, "we don't support Expect:"); @@ -684,6 +697,9 @@ bozo_read_request(bozohttpd_t *httpd) else if (strcasecmp(hdr->h_header, "if-modified-since") == 0) request->hr_if_modified_since = hdr->h_value; + else if (strcasecmp(hdr->h_header, + "accept-encoding") == 0) + request->hr_accept_encoding = hdr->h_value; debug((httpd, DEBUG_FAT, "adding header %s: %s", hdr->h_header, hdr->h_value)); @@ -704,8 +720,9 @@ next_header: goto cleanup; } - /* HTTP/1.1 draft rev-06, 14.23 & 19.6.1.1 */ + /* RFC 2616 (HTTP/1.1), 14.23 & 19.6.1.1 */ if (request->hr_proto == httpd->consts.http_11 && + /*(strncasecmp(request->hr_file, "http://", 7) != 0) &&*/ request->hr_host == NULL) { (void)bozo_http_error(httpd, 400, request, "missing Host header"); @@ -832,6 +849,70 @@ parse_http_date(const char *val, time_t } /* + * given an url, encode it ala rfc 3986. ie, escape ? and friends. + * note that this function returns a static buffer, and thus needs + * to be updated for any sort of parallel processing. + */ +char * +bozo_escape_rfc3986(bozohttpd_t *httpd, const char *url) +{ + static char *buf; + static size_t buflen = 0; + size_t len; + const char *s; + char *d; + + len = strlen(url); + if (buflen < len * 3 + 1) { + buflen = len * 3 + 1; + buf = bozorealloc(httpd, buf, buflen); + } + + if (url == NULL) { + buf[0] = 0; + return buf; + } + + for (len = 0, s = url, d = buf; *s;) { + if (*s & 0x80) + goto encode_it; + switch (*s) { + case ':': + case '/': + case '?': + case '#': + case '[': + case ']': + case '@': + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + case '%': + encode_it: + snprintf(d, 4, "%%%2X", *s++); + d += 3; + len += 3; + break; + default: + *d++ = *s++; + len++; + break; + } + } + buf[len] = 0; + + return buf; +} + +/* * checks to see if this request has a valid .bzdirect file. returns * 0 on failure and 1 on success. */ @@ -843,7 +924,7 @@ check_direct_access(bozo_httpreq_t *requ char dir[MAXPATHLEN], dirfile[MAXPATHLEN], *basename; snprintf(dir, sizeof(dir), "%s", request->hr_file + 1); - debug((request->hr_httpd, DEBUG_FAT, "check_bzredirect: dir %s", dir)); + debug((request->hr_httpd, DEBUG_FAT, "check_direct_access: dir %s", dir)); basename = strrchr(dir, '/'); if ((!basename || basename[1] != '\0') && @@ -875,37 +956,41 @@ handle_redirect(bozo_httpreq_t *request, bozohttpd_t *httpd = request->hr_httpd; char *urlbuf; char portbuf[20]; + const char *hostname = BOZOHOST(httpd, request); int query = 0; - + if (url == NULL) { if (asprintf(&urlbuf, "/%s/", request->hr_file) < 0) bozo_err(httpd, 1, "asprintf"); url = urlbuf; } else urlbuf = NULL; + url = bozo_escape_rfc3986(request->hr_httpd, url); - if (request->hr_query && strlen(request->hr_query)) { + if (request->hr_query && strlen(request->hr_query)) query = 1; - } if (request->hr_serverport && strcmp(request->hr_serverport, "80") != 0) snprintf(portbuf, sizeof(portbuf), ":%s", request->hr_serverport); else portbuf[0] = '\0'; - bozo_warn(httpd, "redirecting %s%s%s", httpd->virthostname, portbuf, url); + if (absolute) + bozo_warn(httpd, "redirecting %s", url); + else + bozo_warn(httpd, "redirecting %s%s%s", hostname, portbuf, url); debug((httpd, DEBUG_FAT, "redirecting %s", url)); bozo_printf(httpd, "%s 301 Document Moved\r\n", request->hr_proto); - if (request->hr_proto != httpd->consts.http_09) + if (request->hr_proto != httpd->consts.http_09) bozo_print_header(request, NULL, "text/html", NULL); if (request->hr_proto != httpd->consts.http_09) { bozo_printf(httpd, "Location: http://"); if (absolute == 0) - bozo_printf(httpd, "%s%s", httpd->virthostname, portbuf); + bozo_printf(httpd, "%s%s", hostname, portbuf); if (query) { - bozo_printf(httpd, "%s?%s\r\n", url, request->hr_query); + bozo_printf(httpd, "%s?%s\r\n", url, request->hr_query); } else { - bozo_printf(httpd, "%s\r\n", url); + bozo_printf(httpd, "%s\r\n", url); } } bozo_printf(httpd, "\r\n"); @@ -915,23 +1000,23 @@ handle_redirect(bozo_httpreq_t *request, bozo_printf(httpd, "

Document Moved

\n"); bozo_printf(httpd, "This document had moved hr_query); - else - bozo_printf(httpd, "%s%s%s?%s", httpd->virthostname, portbuf, url, - request->hr_query); - } else { - if (absolute) - bozo_printf(httpd, "%s", url); - else - bozo_printf(httpd, "%s%s%s", httpd->virthostname, portbuf, url); + if (absolute) + bozo_printf(httpd, "%s?%s", url, request->hr_query); + else + bozo_printf(httpd, "%s%s%s?%s", hostname, + portbuf, url, request->hr_query); + } else { + if (absolute) + bozo_printf(httpd, "%s", url); + else + bozo_printf(httpd, "%s%s%s", hostname, + portbuf, url); } bozo_printf(httpd, "\">here\n"); bozo_printf(httpd, "\n"); head: bozo_flush(httpd, stdout); - if (urlbuf) - free(urlbuf); + free(urlbuf); } /* @@ -959,9 +1044,13 @@ check_virtual(bozo_httpreq_t *request) if (strncasecmp(file, "http://", 7) == 0) { /* we would do virtual hosting here? */ file += 7; + /* RFC 2616 (HTTP/1.1), 5.2: URI takes precedence over Host: */ + free(request->hr_host); + request->hr_host = bozostrdup(request->hr_httpd, file); + if ((s = strchr(request->hr_host, '/')) != NULL) + *s = '\0'; s = strchr(file, '/'); - /* HTTP/1.1 draft rev-06, 5.2: URI takes precedence over Host: */ - request->hr_host = file; + free(request->hr_file); request->hr_file = bozostrdup(request->hr_httpd, s ? s : "/"); debug((httpd, DEBUG_OBESE, "got host ``%s'' file is now ``%s''", request->hr_host, request->hr_file)); @@ -969,12 +1058,20 @@ check_virtual(bozo_httpreq_t *request) goto use_slashdir; /* - * ok, we have a virtual host, use scandir(3) to find a case + * canonicalise hr_host - that is, remove any :80. + */ + len = strlen(request->hr_host); + if (len > 3 && strcmp(request->hr_host + len - 3, ":80") == 0) { + request->hr_host[len - 3] = '\0'; + len = strlen(request->hr_host); + } + + /* + * ok, we have a virtual host, use opendir(3) to find a case * insensitive match for the virtual host we are asked for. * note that if the virtual host is the same as the master, * we don't need to do anything special. */ - len = strlen(request->hr_host); debug((httpd, DEBUG_OBESE, "check_virtual: checking host `%s' under httpd->virtbase `%s' " "for file `%s'", @@ -996,9 +1093,10 @@ check_virtual(bozo_httpreq_t *request) len) == 0) { /* found it, punch it */ debug((httpd, DEBUG_OBESE, "found it punch it")); - httpd->virthostname = d->d_name; + request->hr_virthostname = + bozostrdup(httpd, d->d_name); if (asprintf(&s, "%s/%s", httpd->virtbase, - httpd->virthostname) < 0) + request->hr_virthostname) < 0) bozo_err(httpd, 1, "asprintf"); break; } @@ -1030,13 +1128,15 @@ use_slashdir: /* * checks to see if this request has a valid .bzredirect file. returns - * 0 on failure and 1 on success. + * 0 when no redirection happend, or 1 when handle_redirect() has been + * called. */ -static void +static int check_bzredirect(bozo_httpreq_t *request) { struct stat sb; - char dir[MAXPATHLEN], redir[MAXPATHLEN], redirpath[MAXPATHLEN + 1]; + char dir[MAXPATHLEN], redir[MAXPATHLEN], redirpath[MAXPATHLEN + 1], + path[MAXPATHLEN]; char *basename, *finalredir; int rv, absolute; @@ -1061,12 +1161,12 @@ check_bzredirect(bozo_httpreq_t *request snprintf(redir, sizeof(redir), "%s/%s", dir, REDIRECT_FILE); if (lstat(redir, &sb) == 0) { if (!S_ISLNK(sb.st_mode)) - return; + return 0; absolute = 0; } else { snprintf(redir, sizeof(redir), "%s/%s", dir, ABSREDIRECT_FILE); if (lstat(redir, &sb) < 0 || !S_ISLNK(sb.st_mode)) - return; + return 0; absolute = 1; } debug((request->hr_httpd, DEBUG_FAT, @@ -1074,12 +1174,17 @@ check_bzredirect(bozo_httpreq_t *request rv = readlink(redir, redirpath, sizeof redirpath - 1); if (rv == -1 || rv == 0) { debug((request->hr_httpd, DEBUG_FAT, "readlink failed")); - return; + return 0; } redirpath[rv] = '\0'; debug((request->hr_httpd, DEBUG_FAT, "readlink returned \"%s\"", redirpath)); - + + /* check if we need authentication */ + snprintf(path, sizeof(path), "%s/", dir); + if (bozo_auth_check(request, path)) + return 1; + /* now we have the link pointer, redirect to the real place */ if (absolute) finalredir = redirpath; @@ -1090,6 +1195,7 @@ check_bzredirect(bozo_httpreq_t *request debug((request->hr_httpd, DEBUG_FAT, "check_bzredirect: new redir %s", finalredir)); handle_redirect(request, finalredir, absolute); + return 1; } /* this fixes the %HH hack that RFC2396 requires. */ @@ -1135,7 +1241,7 @@ fix_url_percent(bozo_httpreq_t *request) "percent hack was %2f (/)"); goto copy_rest; } - + buf[0] = *++s; buf[1] = *++s; buf[2] = '\0'; @@ -1172,7 +1278,7 @@ copy_rest: * - punt if it doesn't start with / * - check httpd->untrustedref / referrer * - look for "http://myname/" and deal with it. - * - maybe call bozo_process_cgi() + * - maybe call bozo_process_cgi() * - check for ~user and call bozo_user_transform() if so * - if the length > 1, check for trailing slash. if so, * add the index.html file @@ -1188,6 +1294,7 @@ transform_request(bozo_httpreq_t *reques bozohttpd_t *httpd = request->hr_httpd; char *file, *newfile = NULL; size_t len; + const char *hostname = BOZOHOST(httpd, request); file = NULL; *isindex = 0; @@ -1203,15 +1310,16 @@ transform_request(bozo_httpreq_t *reques goto bad_done; } - check_bzredirect(request); + if (check_bzredirect(request)) + return 0; if (httpd->untrustedref) { int to_indexhtml = 0; #define TOP_PAGE(x) (strcmp((x), "/") == 0 || \ strcmp((x) + 1, httpd->index_html) == 0 || \ - strcmp((x) + 1, "favicon.ico") == 0) - + strcmp((x) + 1, "favicon.ico") == 0) + debug((httpd, DEBUG_EXPLODING, "checking httpd->untrustedref")); /* * first check that this path isn't allowed via .bzdirect file, @@ -1226,10 +1334,10 @@ transform_request(bozo_httpreq_t *reques debug((httpd, DEBUG_FAT, "checking referrer \"%s\" vs virthostname %s", - r, httpd->virthostname)); + r, hostname)); if (strncmp(r, "http://", 7) != 0 || - (strncasecmp(r + 7, httpd->virthostname, - strlen(httpd->virthostname)) != 0 && + (strncasecmp(r + 7, hostname, + strlen(hostname)) != 0 && !TOP_PAGE(file))) to_indexhtml = 1; } else { @@ -1238,8 +1346,8 @@ transform_request(bozo_httpreq_t *reques debug((httpd, DEBUG_FAT, "url has no referrer at all")); /* if there's no referrer, let / or /index.html past */ if (!TOP_PAGE(file) || - (h && strncasecmp(h, httpd->virthostname, - strlen(httpd->virthostname)) != 0)) + (h && strncasecmp(h, hostname, + strlen(hostname)) != 0)) to_indexhtml = 1; } @@ -1306,7 +1414,7 @@ transform_request(bozo_httpreq_t *reques */ /* - * stop traversing outside our domain + * stop traversing outside our domain * * XXX true security only comes from our parent using chroot(2) * before execve(2)'ing us. or our own built in chroot(2) support. @@ -1328,12 +1436,61 @@ transform_request(bozo_httpreq_t *reques if (bozo_process_cgi(request)) return 0; + if (bozo_process_lua(request)) + return 0; + debug((httpd, DEBUG_FAT, "transform_request set: %s", newfile)); return 1; bad_done: debug((httpd, DEBUG_FAT, "transform_request returning: 0")); - if (newfile) - free(newfile); + free(newfile); + return 0; +} + +/* + * can_gzip checks if the request supports and prefers gzip encoding. + * + * XXX: we do not consider the associated q with gzip in making our + * decision which is broken. + */ + +static int +can_gzip(bozo_httpreq_t *request) +{ + const char *pos; + const char *tmp; + size_t len; + + /* First we decide if the request can be gzipped at all. */ + + /* not if we already are encoded... */ + tmp = bozo_content_encoding(request, request->hr_file); + if (tmp && *tmp) + return 0; + + /* not if we are not asking for the whole file... */ + if (request->hr_last_byte_pos != -1 || request->hr_have_range) + return 0; + + /* Then we determine if gzip is on the cards. */ + + for (pos = request->hr_accept_encoding; pos && *pos; pos += len) { + while (*pos == ' ') + pos++; + + len = strcspn(pos, ";,"); + + if ((len == 4 && strncasecmp("gzip", pos, 4) == 0) || + (len == 6 && strncasecmp("x-gzip", pos, 6) == 0)) + return 1; + + if (pos[len] == ';') + len += strcspn(&pos[len], ","); + + if (pos[len]) + len++; + } + return 0; } @@ -1362,16 +1519,28 @@ bozo_process_request(bozo_httpreq_t *req if (transform_request(request, &isindex) == 0) return; + fd = -1; + encoding = NULL; + if (can_gzip(request)) { + asprintf(&file, "%s.gz", request->hr_file); + fd = open(file, O_RDONLY); + if (fd >= 0) + encoding = "gzip"; + free(file); + } + file = request->hr_file; - fd = open(file, O_RDONLY); + if (fd < 0) + fd = open(file, O_RDONLY); + if (fd < 0) { debug((httpd, DEBUG_FAT, "open failed: %s", strerror(errno))); if (errno == EPERM) (void)bozo_http_error(httpd, 403, request, "no permission to open file"); else if (errno == ENOENT) { - if (!bozo_dir_index(request, file, isindex)) + if (!bozo_dir_index(request, file, isindex)) (void)bozo_http_error(httpd, 404, request, "no file"); } else @@ -1420,7 +1589,8 @@ bozo_process_request(bozo_httpreq_t *req if (request->hr_proto != httpd->consts.http_09) { type = bozo_content_type(request, file); - encoding = bozo_content_encoding(request, file); + if (!encoding) + encoding = bozo_content_encoding(request, file); bozo_print_header(request, &sb, type, encoding); bozo_printf(httpd, "\r\n"); @@ -1533,7 +1703,7 @@ debug__(bozohttpd_t *httpd, int level, c { va_list ap; int savederrno; - + /* only log if the level is low enough */ if (httpd->debug < level) return; @@ -1582,12 +1752,20 @@ bozo_err(bozohttpd_t *httpd, int code, c exit(code); } -/* this escape HTML tags */ -static void -escape_html(bozo_httpreq_t *request) +/* + * this escapes HTML tags. returns allocated escaped + * string if needed, or NULL on allocation failure or + * lack of escape need. + * call with NULL httpd in error paths, to avoid recursive + * malloc failure. call with valid httpd in normal paths + * to get automatic allocation failure handling. + */ +char * +bozo_escape_html(bozohttpd_t *httpd, const char *url) { int i, j; - char *url = request->hr_file, *tmp; + char *tmp; + size_t len; for (i = 0, j = 0; url[i]; i++) { switch (url[i]) { @@ -1602,16 +1780,17 @@ escape_html(bozo_httpreq_t *request) } if (j == 0) - return; + return NULL; - if ((tmp = (char *) malloc(strlen(url) + j)) == 0) - /* - * ouch, but we are only called from an error context, and - * most paths here come from malloc(3) failures anyway... - * we could completely punt and just exit, but isn't returning - * an not-quite-correct error better than nothing at all? - */ - return; + /* + * we need to handle being called from different + * pathnames. + */ + len = strlen(url) + j; + if (httpd) + tmp = bozomalloc(httpd, len); + else if ((tmp = malloc(len)) == 0) + return NULL; for (i = 0, j = 0; url[i]; i++) { switch (url[i]) { @@ -1633,8 +1812,7 @@ escape_html(bozo_httpreq_t *request) } tmp[j] = 0; - free(request->hr_file); - request->hr_file = tmp; + return tmp; } /* short map between error code, and short/long messages */ @@ -1707,15 +1885,21 @@ bozo_http_error(bozohttpd_t *httpd, int portbuf[0] = '\0'; if (request && request->hr_file) { - escape_html(request); + char *file = NULL; + const char *hostname = BOZOHOST(httpd, request); + + /* bozo_escape_html() failure here is just too bad. */ + file = bozo_escape_html(NULL, request->hr_file); + if (file == NULL) + file = request->hr_file; size = snprintf(httpd->errorbuf, BUFSIZ, "%s\n" "

%s

\n" "%s:
%s
\n" "
%s%s
\n" "\n", - header, header, request->hr_file, reason, - httpd->virthostname, portbuf, httpd->virthostname, portbuf); + header, header, file, reason, + hostname, portbuf, hostname, portbuf); if (size >= (int)BUFSIZ) { bozo_warn(httpd, "bozo_http_error buffer too small, truncated"); @@ -1915,6 +2099,9 @@ bozo_init_httpd(bozohttpd_t *httpd) "bozohttpd: memory_allocation failure\n"); return 0; } +#ifndef NO_LUA_SUPPORT + SIMPLEQ_INIT(&httpd->lua_states); +#endif return 1; }