From 30fff08a44094df9b775f2e4bf9f5abceb847415 Mon Sep 17 00:00:00 2001 From: Felix Fietkau Date: Sun, 30 Dec 2012 19:36:18 +0100 Subject: [PATCH 1/1] Initial implementation --- CMakeLists.txt | 12 + client.c | 332 ++++++++++++++++++++++++ file.c | 630 +++++++++++++++++++++++++++++++++++++++++++++ listen.c | 177 +++++++++++++ main.c | 105 ++++++++ uhttpd-mimetypes.h | 91 +++++++ uhttpd.h | 149 +++++++++++ utils.c | 210 +++++++++++++++ utils.h | 58 +++++ 9 files changed, 1764 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 client.c create mode 100644 file.c create mode 100644 listen.c create mode 100644 main.c create mode 100644 uhttpd-mimetypes.h create mode 100644 uhttpd.h create mode 100644 utils.c create mode 100644 utils.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3489414 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 2.6) + +PROJECT(uhttpd C) +ADD_DEFINITIONS(-O1 -Wall -Werror --std=gnu99 -g3) + +IF(APPLE) + INCLUDE_DIRECTORIES(/opt/local/include) + LINK_DIRECTORIES(/opt/local/lib) +ENDIF() + +ADD_EXECUTABLE(uhttpd main.c listen.c client.c utils.c file.c) +TARGET_LINK_LIBRARIES(uhttpd ubox ubus) diff --git a/client.c b/client.c new file mode 100644 index 0000000..9e387c7 --- /dev/null +++ b/client.c @@ -0,0 +1,332 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "uhttpd.h" + +static LIST_HEAD(clients); + +int n_clients = 0; +struct config conf = {}; + +static const char *http_versions[] = { + [UH_HTTP_VER_0_9] = "HTTP/0.9", + [UH_HTTP_VER_1_0] = "HTTP/1.0", + [UH_HTTP_VER_1_1] = "HTTP/1.1", +}; + +void uh_http_header(struct client *cl, int code, const char *summary) +{ + const char *enc = "Transfer-Encoding: chunked\r\n"; + const char *conn; + + if (!uh_use_chunked(cl)) + enc = ""; + + if (cl->request.version != UH_HTTP_VER_1_1) + conn = "Connection: close"; + else + conn = "Connection: keep-alive"; + + ustream_printf(cl->us, "%s %03i %s\r\n%s\r\n%s", + http_versions[cl->request.version], + code, summary, conn, enc); +} + +static void uh_client_error_header(struct client *cl, int code, const char *summary) +{ + uh_http_header(cl, code, summary); + ustream_printf(cl->us, "Content-Type: text/plain\r\n\r\n"); +} + +static void uh_connection_close(struct client *cl) +{ + cl->state = CLIENT_STATE_DONE; + cl->us->eof = true; + ustream_state_change(cl->us); +} + +static void uh_dispatch_done(struct client *cl) +{ + if (cl->dispatch_free) + cl->dispatch_free(cl); + cl->dispatch_free = NULL; +} + +void uh_request_done(struct client *cl) +{ + uh_chunk_eof(cl); + uh_dispatch_done(cl); + cl->us->notify_write = NULL; + memset(&cl->data, 0, sizeof(cl->data)); + + if (cl->request.version != UH_HTTP_VER_1_1 || !conf.http_keepalive) { + uh_connection_close(cl); + return; + } + + cl->state = CLIENT_STATE_INIT; + uloop_timeout_set(&cl->timeout, conf.http_keepalive * 1000); +} + +void __printf(4, 5) +uh_client_error(struct client *cl, int code, const char *summary, const char *fmt, ...) +{ + va_list arg; + + uh_client_error_header(cl, code, summary); + + va_start(arg, fmt); + uh_chunk_vprintf(cl, fmt, arg); + va_end(arg); + + uh_request_done(cl); +} + +static void uh_header_error(struct client *cl, int code, const char *summary) +{ + uh_client_error(cl, code, summary, "%s", summary); + uh_connection_close(cl); +} + +static void client_timeout(struct uloop_timeout *timeout) +{ + struct client *cl = container_of(timeout, struct client, timeout); + + cl->state = CLIENT_STATE_CLOSE; + uh_connection_close(cl); +} + +static int client_parse_request(struct client *cl, char *data) +{ + struct http_request *req = &cl->request; + char *type, *path, *version; + int i; + + type = strtok(data, " "); + path = strtok(NULL, " "); + version = strtok(NULL, " "); + if (!type || !path || !version) + return CLIENT_STATE_DONE; + + req->url = path; + if (!strcmp(type, "GET")) + req->method = UH_HTTP_MSG_GET; + else if (!strcmp(type, "POST")) + req->method = UH_HTTP_MSG_POST; + else if (!strcmp(type, "HEAD")) + req->method = UH_HTTP_MSG_HEAD; + else + return CLIENT_STATE_DONE; + + cl->request.version = -1; + i = array_size(http_versions); + while (i--) { + if (!strcmp(version, http_versions[i])) { + cl->request.version = i; + break; + } + } + if (cl->request.version < 0) + return CLIENT_STATE_DONE; + + return CLIENT_STATE_HEADER; +} + +static bool client_init_cb(struct client *cl, char *buf, int len) +{ + char *newline; + + newline = strstr(buf, "\r\n"); + if (!newline) + return false; + + *newline = 0; + blob_buf_init(&cl->hdr, 0); + blobmsg_add_string(&cl->hdr, "REQUEST", buf); + ustream_consume(cl->us, newline + 2 - buf); + cl->state = client_parse_request(cl, (char *) blobmsg_data(blob_data(cl->hdr.head))); + if (cl->state == CLIENT_STATE_DONE) + uh_header_error(cl, 400, "Bad Request"); + + return true; +} + +static void client_header_complete(struct client *cl) +{ + uh_handle_file_request(cl); +} + +static int client_parse_header(struct client *cl, char *data) +{ + char *name; + char *val; + + if (!*data) { + uloop_timeout_cancel(&cl->timeout); + client_header_complete(cl); + return CLIENT_STATE_DATA; + } + + val = strchr(data, ':'); + if (!val) + return CLIENT_STATE_DONE; + + *val = 0; + val++; + + while (isspace(*val)) + val++; + + for (name = data; *name; name++) + if (isupper(*name)) + *name = tolower(*name); + + blobmsg_add_string(&cl->hdr, data, val); + + return CLIENT_STATE_HEADER; +} + +static bool client_data_cb(struct client *cl, char *buf, int len) +{ + return false; +} + +static bool client_header_cb(struct client *cl, char *buf, int len) +{ + char *newline; + int line_len; + + newline = strstr(buf, "\r\n"); + if (!newline) + return false; + + *newline = 0; + cl->state = client_parse_header(cl, buf); + line_len = newline + 2 - buf; + ustream_consume(cl->us, line_len); + if (cl->state == CLIENT_STATE_DATA) + client_data_cb(cl, newline + 2, len - line_len); + + return true; +} + +typedef bool (*read_cb_t)(struct client *cl, char *buf, int len); +static read_cb_t read_cbs[] = { + [CLIENT_STATE_INIT] = client_init_cb, + [CLIENT_STATE_HEADER] = client_header_cb, + [CLIENT_STATE_DATA] = client_data_cb, +}; + +static void client_read_cb(struct client *cl) +{ + struct ustream *us = cl->us; + char *str; + int len; + + do { + str = ustream_get_read_buf(us, &len); + if (!str) + break; + + if (cl->state >= array_size(read_cbs) || !read_cbs[cl->state]) + break; + + if (!read_cbs[cl->state](cl, str, len)) { + if (len == us->r.buffer_len) + uh_header_error(cl, 413, "Request Entity Too Large"); + break; + } + } while(1); +} + +static void client_close(struct client *cl) +{ + uh_dispatch_done(cl); + uloop_timeout_cancel(&cl->timeout); + ustream_free(&cl->sfd.stream); + close(cl->sfd.fd.fd); + list_del(&cl->list); + free(cl); + + uh_unblock_listeners(); +} + +static void client_ustream_read_cb(struct ustream *s, int bytes) +{ + struct client *cl = container_of(s, struct client, sfd); + + client_read_cb(cl); +} + +static void client_ustream_write_cb(struct ustream *s, int bytes) +{ + struct client *cl = container_of(s, struct client, sfd); + + if (cl->dispatch_write_cb) + cl->dispatch_write_cb(cl); +} + +static void client_notify_state(struct ustream *s) +{ + struct client *cl = container_of(s, struct client, sfd); + + if (cl->state == CLIENT_STATE_CLOSE || + (s->eof && !s->w.data_bytes) || s->write_error) + return client_close(cl); +} + +void uh_accept_client(int fd) +{ + static struct client *next_client; + struct client *cl; + unsigned int sl; + int sfd; + static int client_id = 0; + + if (!next_client) + next_client = calloc(1, sizeof(*next_client)); + + cl = next_client; + + sl = sizeof(cl->peeraddr); + sfd = accept(fd, (struct sockaddr *) &cl->peeraddr, &sl); + if (sfd < 0) + return; + + sl = sizeof(cl->servaddr); + getsockname(fd, (struct sockaddr *) &cl->servaddr, &sl); + cl->us = &cl->sfd.stream; + cl->us->string_data = true; + cl->us->notify_read = client_ustream_read_cb; + cl->us->notify_write = client_ustream_write_cb; + cl->us->notify_state = client_notify_state; + ustream_fd_init(&cl->sfd, sfd); + + cl->timeout.cb = client_timeout; + uloop_timeout_set(&cl->timeout, conf.network_timeout * 1000); + + list_add_tail(&cl->list, &clients); + + next_client = NULL; + n_clients++; + cl->id = client_id++; +} diff --git a/file.c b/file.c new file mode 100644 index 0000000..f1f5d6d --- /dev/null +++ b/file.c @@ -0,0 +1,630 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include "uhttpd.h" +#include "uhttpd-mimetypes.h" + +static LIST_HEAD(index_files); + +struct index_file { + struct list_head list; + const char *name; +}; + +struct path_info { + char *root; + char *phys; + char *name; + char *info; + char *query; + int redirected; + struct stat stat; +}; + +enum file_hdr { + HDR_IF_MODIFIED_SINCE, + HDR_IF_UNMODIFIED_SINCE, + HDR_IF_MATCH, + HDR_IF_NONE_MATCH, + HDR_IF_RANGE, + __HDR_MAX +}; + +void uh_index_add(const char *filename) +{ + struct index_file *idx; + + idx = calloc(1, sizeof(*idx)); + idx->name = filename; + list_add_tail(&idx->list, &index_files); +} + +static char * canonpath(const char *path, char *path_resolved) +{ + char path_copy[PATH_MAX]; + char *path_cpy = path_copy; + char *path_res = path_resolved; + struct stat s; + + /* relative -> absolute */ + if (*path != '/') { + getcwd(path_copy, PATH_MAX); + strncat(path_copy, "/", PATH_MAX - strlen(path_copy)); + strncat(path_copy, path, PATH_MAX - strlen(path_copy)); + } else { + strncpy(path_copy, path, PATH_MAX); + } + + /* normalize */ + while ((*path_cpy != '\0') && (path_cpy < (path_copy + PATH_MAX - 2))) { + if (*path_cpy != '/') + goto next; + + /* skip repeating / */ + if (path_cpy[1] == '/') { + path_cpy++; + continue; + } + + /* /./ or /../ */ + if (path_cpy[1] == '.') { + /* skip /./ */ + if ((path_cpy[2] == '/') || (path_cpy[2] == '\0')) { + path_cpy += 2; + continue; + } + + /* collapse /x/../ */ + if ((path_cpy[2] == '.') && + ((path_cpy[3] == '/') || (path_cpy[3] == '\0'))) { + while ((path_res > path_resolved) && (*--path_res != '/')); + + path_cpy += 3; + continue; + } + } + +next: + *path_res++ = *path_cpy++; + } + + /* remove trailing slash if not root / */ + if ((path_res > (path_resolved+1)) && (path_res[-1] == '/')) + path_res--; + else if (path_res == path_resolved) + *path_res++ = '/'; + + *path_res = '\0'; + + /* test access */ + if (!stat(path_resolved, &s) && (s.st_mode & S_IROTH)) + return path_resolved; + + return NULL; +} + +/* Returns NULL on error. +** NB: improperly encoded URL should give client 400 [Bad Syntax]; returning +** NULL here causes 404 [Not Found], but that's not too unreasonable. */ +struct path_info * uh_path_lookup(struct client *cl, const char *url) +{ + static char path_phys[PATH_MAX]; + static char path_info[PATH_MAX]; + static struct path_info p; + + char buffer[UH_LIMIT_MSGHEAD]; + char *docroot = conf.docroot; + char *pathptr = NULL; + + int slash = 0; + int no_sym = conf.no_symlinks; + int i = 0; + struct stat s; + struct index_file *idx; + + /* back out early if url is undefined */ + if (url == NULL) + return NULL; + + memset(path_phys, 0, sizeof(path_phys)); + memset(path_info, 0, sizeof(path_info)); + memset(buffer, 0, sizeof(buffer)); + memset(&p, 0, sizeof(p)); + + /* copy docroot */ + memcpy(buffer, docroot, + min(strlen(docroot), sizeof(buffer) - 1)); + + /* separate query string from url */ + if ((pathptr = strchr(url, '?')) != NULL) { + p.query = pathptr[1] ? pathptr + 1 : NULL; + + /* urldecode component w/o query */ + if (pathptr > url) { + if (uh_urldecode(&buffer[strlen(docroot)], + sizeof(buffer) - strlen(docroot) - 1, + url, pathptr - url ) < 0) + return NULL; /* bad URL */ + } + } + + /* no query string, decode all of url */ + else if (uh_urldecode(&buffer[strlen(docroot)], + sizeof(buffer) - strlen(docroot) - 1, + url, strlen(url) ) < 0) + return NULL; /* bad URL */ + + /* create canon path */ + for (i = strlen(buffer), slash = (buffer[max(0, i-1)] == '/'); i >= 0; i--) { + if ((buffer[i] == 0) || (buffer[i] == '/')) { + memset(path_info, 0, sizeof(path_info)); + memcpy(path_info, buffer, min(i + 1, sizeof(path_info) - 1)); + + if (no_sym ? realpath(path_info, path_phys) + : canonpath(path_info, path_phys)) { + memset(path_info, 0, sizeof(path_info)); + memcpy(path_info, &buffer[i], + min(strlen(buffer) - i, sizeof(path_info) - 1)); + + break; + } + } + } + + /* check whether found path is within docroot */ + if (strncmp(path_phys, docroot, strlen(docroot)) || + ((path_phys[strlen(docroot)] != 0) && + (path_phys[strlen(docroot)] != '/'))) + return NULL; + + /* test current path */ + if (!stat(path_phys, &p.stat)) { + /* is a regular file */ + if (p.stat.st_mode & S_IFREG) { + p.root = docroot; + p.phys = path_phys; + p.name = &path_phys[strlen(docroot)]; + p.info = path_info[0] ? path_info : NULL; + } + + /* is a directory */ + else if ((p.stat.st_mode & S_IFDIR) && !strlen(path_info)) { + /* ensure trailing slash */ + if (path_phys[strlen(path_phys)-1] != '/') + path_phys[strlen(path_phys)] = '/'; + + /* try to locate index file */ + memset(buffer, 0, sizeof(buffer)); + memcpy(buffer, path_phys, sizeof(buffer)); + pathptr = &buffer[strlen(buffer)]; + + /* if requested url resolves to a directory and a trailing slash + is missing in the request url, redirect the client to the same + url with trailing slash appended */ + if (!slash) { + uh_http_header(cl, 302, "Found"); + ustream_printf(cl->us, "Location: %s%s%s\r\n\r\n", + &path_phys[strlen(docroot)], + p.query ? "?" : "", + p.query ? p.query : ""); + uh_request_done(cl); + p.redirected = 1; + } else { + list_for_each_entry(idx, &index_files, list) { + strncat(buffer, idx->name, sizeof(buffer)); + + if (!stat(buffer, &s) && (s.st_mode & S_IFREG)) { + memcpy(path_phys, buffer, sizeof(path_phys)); + memcpy(&p.stat, &s, sizeof(p.stat)); + break; + } + + *pathptr = 0; + } + } + + p.root = docroot; + p.phys = path_phys; + p.name = &path_phys[strlen(docroot)]; + } + } + + return p.phys ? &p : NULL; +} + +#ifdef __APPLE__ +time_t timegm (struct tm *tm); +#endif + +static const char * uh_file_mime_lookup(const char *path) +{ + struct mimetype *m = &uh_mime_types[0]; + const char *e; + + while (m->extn) { + e = &path[strlen(path)-1]; + + while (e >= path) { + if ((*e == '.' || *e == '/') && !strcasecmp(&e[1], m->extn)) + return m->mime; + + e--; + } + + m++; + } + + return "application/octet-stream"; +} + +static const char * uh_file_mktag(struct stat *s) +{ + static char tag[128]; + + snprintf(tag, sizeof(tag), "\"%x-%x-%x\"", + (unsigned int) s->st_ino, + (unsigned int) s->st_size, + (unsigned int) s->st_mtime); + + return tag; +} + +static time_t uh_file_date2unix(const char *date) +{ + struct tm t; + + memset(&t, 0, sizeof(t)); + + if (strptime(date, "%a, %d %b %Y %H:%M:%S %Z", &t) != NULL) + return timegm(&t); + + return 0; +} + +static char * uh_file_unix2date(time_t ts) +{ + static char str[128]; + struct tm *t = gmtime(&ts); + + strftime(str, sizeof(str), "%a, %d %b %Y %H:%M:%S GMT", t); + + return str; +} + +static char *uh_file_header(struct client *cl, int idx) +{ + if (!cl->data.file.hdr[idx]) + return NULL; + + return (char *) blobmsg_data(cl->data.file.hdr[idx]); +} + +static void uh_file_response_ok_hdrs(struct client *cl, struct stat *s) +{ + if (s) { + ustream_printf(cl->us, "ETag: %s\r\n", uh_file_mktag(s)); + ustream_printf(cl->us, "Last-Modified: %s\r\n", + uh_file_unix2date(s->st_mtime)); + } + ustream_printf(cl->us, "Date: %s\r\n", uh_file_unix2date(time(NULL))); +} + +static void uh_file_response_200(struct client *cl, struct stat *s) +{ + uh_http_header(cl, 200, "OK"); + return uh_file_response_ok_hdrs(cl, s); +} + +static void uh_file_response_304(struct client *cl, struct stat *s) +{ + uh_http_header(cl, 304, "Not Modified"); + + return uh_file_response_ok_hdrs(cl, s); +} + +static void uh_file_response_412(struct client *cl) +{ + uh_http_header(cl, 412, "Precondition Failed"); +} + +static bool uh_file_if_match(struct client *cl, struct stat *s) +{ + const char *tag = uh_file_mktag(s); + char *hdr = uh_file_header(cl, HDR_IF_MATCH); + char *p; + int i; + + if (!hdr) + return true; + + p = &hdr[0]; + for (i = 0; i < strlen(hdr); i++) + { + if ((hdr[i] == ' ') || (hdr[i] == ',')) { + hdr[i++] = 0; + p = &hdr[i]; + } else if (!strcmp(p, "*") || !strcmp(p, tag)) { + return true; + } + } + + uh_file_response_412(cl); + return false; +} + +static int uh_file_if_modified_since(struct client *cl, struct stat *s) +{ + char *hdr = uh_file_header(cl, HDR_IF_MODIFIED_SINCE); + + if (!hdr) + return true; + + if (uh_file_date2unix(hdr) >= s->st_mtime) { + uh_file_response_304(cl, s); + return false; + } + + return true; +} + +static int uh_file_if_none_match(struct client *cl, struct stat *s) +{ + const char *tag = uh_file_mktag(s); + char *hdr = uh_file_header(cl, HDR_IF_NONE_MATCH); + char *p; + int i; + + if (!hdr) + return true; + + p = &hdr[0]; + for (i = 0; i < strlen(hdr); i++) { + if ((hdr[i] == ' ') || (hdr[i] == ',')) { + hdr[i++] = 0; + p = &hdr[i]; + } else if (!strcmp(p, "*") || !strcmp(p, tag)) { + if ((cl->request.method == UH_HTTP_MSG_GET) || + (cl->request.method == UH_HTTP_MSG_HEAD)) + uh_file_response_304(cl, s); + else + uh_file_response_412(cl); + + return false; + } + } + + return true; +} + +static int uh_file_if_range(struct client *cl, struct stat *s) +{ + char *hdr = uh_file_header(cl, HDR_IF_RANGE); + + if (hdr) { + uh_file_response_412(cl); + return false; + } + + return true; +} + +static int uh_file_if_unmodified_since(struct client *cl, struct stat *s) +{ + char *hdr = uh_file_header(cl, HDR_IF_UNMODIFIED_SINCE); + + if (hdr && uh_file_date2unix(hdr) <= s->st_mtime) { + uh_file_response_412(cl); + return false; + } + + return true; +} + + +static int uh_file_scandir_filter_dir(const struct dirent *e) +{ + return strcmp(e->d_name, ".") ? 1 : 0; +} + +static void uh_file_dirlist(struct client *cl, struct path_info *pi) +{ + int i; + int count = 0; + char filename[PATH_MAX]; + char *pathptr; + struct dirent **files = NULL; + struct stat s; + + uh_file_response_200(cl, NULL); + ustream_printf(cl->us, "Content-Type: text/html\r\n\r\n"); + + uh_chunk_printf(cl, + "Index of %s" + "

Index of %s


    ", + pi->name, pi->name); + + if ((count = scandir(pi->phys, &files, uh_file_scandir_filter_dir, + alphasort)) > 0) + { + memset(filename, 0, sizeof(filename)); + memcpy(filename, pi->phys, sizeof(filename)); + pathptr = &filename[strlen(filename)]; + + /* list subdirs */ + for (i = 0; i < count; i++) { + strncat(filename, files[i]->d_name, + sizeof(filename) - strlen(files[i]->d_name)); + + if (!stat(filename, &s) && + (s.st_mode & S_IFDIR) && (s.st_mode & S_IXOTH)) + uh_chunk_printf(cl, + "
  1. %s/" + "
    modified: %s" + "
    directory - %.02f kbyte
    " + "
  2. ", + pi->name, files[i]->d_name, + files[i]->d_name, + uh_file_unix2date(s.st_mtime), + s.st_size / 1024.0); + + *pathptr = 0; + } + + /* list files */ + for (i = 0; i < count; i++) { + strncat(filename, files[i]->d_name, + sizeof(filename) - strlen(files[i]->d_name)); + + if (!stat(filename, &s) && + !(s.st_mode & S_IFDIR) && (s.st_mode & S_IROTH)) + uh_chunk_printf(cl, + "
  3. %s" + "
    modified: %s" + "
    %s - %.02f kbyte
    " + "
  4. ", + pi->name, files[i]->d_name, + files[i]->d_name, + uh_file_unix2date(s.st_mtime), + uh_file_mime_lookup(filename), + s.st_size / 1024.0); + + *pathptr = 0; + } + } + + uh_chunk_printf(cl, "

"); + uh_request_done(cl); + + if (files) + { + for (i = 0; i < count; i++) + free(files[i]); + + free(files); + } +} + +static void file_write_cb(struct client *cl) +{ + char buf[512]; + int fd = cl->data.file.fd; + int r; + + while (cl->us->w.data_bytes < 256) { + r = read(fd, buf, sizeof(buf)); + if (r < 0) { + if (errno == EINTR) + continue; + } + + if (!r) { + uh_request_done(cl); + return; + } + + uh_chunk_write(cl, buf, r); + } +} + +static void uh_file_free(struct client *cl) +{ + close(cl->data.file.fd); +} + +static void uh_file_data(struct client *cl, struct path_info *pi, int fd) +{ + /* test preconditions */ + if (!uh_file_if_modified_since(cl, &pi->stat) || + !uh_file_if_match(cl, &pi->stat) || + !uh_file_if_range(cl, &pi->stat) || + !uh_file_if_unmodified_since(cl, &pi->stat) || + !uh_file_if_none_match(cl, &pi->stat)) { + uh_request_done(cl); + close(fd); + return; + } + + /* write status */ + uh_file_response_200(cl, &pi->stat); + + ustream_printf(cl->us, "Content-Type: %s\r\n", + uh_file_mime_lookup(pi->name)); + + ustream_printf(cl->us, "Content-Length: %i\r\n\r\n", + pi->stat.st_size); + + + /* send body */ + if (cl->request.method == UH_HTTP_MSG_HEAD) { + uh_request_done(cl); + close(fd); + return; + } + + cl->data.file.fd = fd; + cl->dispatch_write_cb = file_write_cb; + cl->dispatch_free = uh_file_free; + file_write_cb(cl); +} + +static void uh_file_request(struct client *cl, struct path_info *pi) +{ + static const struct blobmsg_policy hdr_policy[__HDR_MAX] = { + [HDR_IF_MODIFIED_SINCE] = { "if-modified-since", BLOBMSG_TYPE_STRING }, + [HDR_IF_UNMODIFIED_SINCE] = { "if-unmodified-since", BLOBMSG_TYPE_STRING }, + [HDR_IF_MATCH] = { "if-match", BLOBMSG_TYPE_STRING }, + [HDR_IF_NONE_MATCH] = { "if-none-match", BLOBMSG_TYPE_STRING }, + [HDR_IF_RANGE] = { "if-range", BLOBMSG_TYPE_STRING }, + }; + struct blob_attr *tb[__HDR_MAX]; + int fd; + + blobmsg_parse(hdr_policy, __HDR_MAX, tb, blob_data(cl->hdr.head), blob_len(cl->hdr.head)); + + cl->data.file.hdr = tb; + if ((pi->stat.st_mode & S_IFREG) && ((fd = open(pi->phys, O_RDONLY)) > 0)) + uh_file_data(cl, pi, fd); + else if ((pi->stat.st_mode & S_IFDIR) && !conf.no_dirlists) + uh_file_dirlist(cl, pi); + else + uh_client_error(cl, 403, "Forbidden", + "Access to this resource is forbidden"); + cl->data.file.hdr = NULL; +} + +void uh_handle_file_request(struct client *cl) +{ + struct path_info *pi; + + pi = uh_path_lookup(cl, cl->request.url); + if (!pi) { + uh_request_done(cl); + return; + } + + if (pi->redirected) + return; + + uh_file_request(cl, pi); +} diff --git a/listen.c b/listen.c new file mode 100644 index 0000000..74a85f9 --- /dev/null +++ b/listen.c @@ -0,0 +1,177 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include "uhttpd.h" + +struct listener { + struct list_head list; + struct uloop_fd fd; + int socket; + int n_clients; + struct sockaddr_in6 addr; + bool tls; + bool blocked; +}; + +static LIST_HEAD(listeners); +static int n_blocked; + +static void uh_block_listener(struct listener *l) +{ + uloop_fd_delete(&l->fd); + n_blocked++; + l->blocked = true; +} + +void uh_unblock_listeners(void) +{ + struct listener *l; + + if (!n_blocked && conf.max_requests && + n_clients >= conf.max_requests) + return; + + list_for_each_entry(l, &listeners, list) { + if (!l->blocked) + continue; + + n_blocked--; + l->blocked = false; + uloop_fd_add(&l->fd, ULOOP_READ); + } +} + +static void listener_cb(struct uloop_fd *fd, unsigned int events) +{ + struct listener *l = container_of(fd, struct listener, fd); + + uh_accept_client(fd->fd); + + if (conf.max_requests && n_clients >= conf.max_requests) + uh_block_listener(l); +} + +void uh_setup_listeners(void) +{ + struct listener *l; + + list_for_each_entry(l, &listeners, list) { + l->fd.cb = listener_cb; + uloop_fd_add(&l->fd, ULOOP_READ); + } +} + +int uh_socket_bind(const char *host, const char *port, bool tls) +{ + int sock = -1; + int yes = 1; + int status; + int bound = 0; + struct listener *l = NULL; + struct addrinfo *addrs = NULL, *p = NULL; + static struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE, + }; + + if ((status = getaddrinfo(host, port, &hints, &addrs)) != 0) { + fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(status)); + return -1; + } + + /* try to bind a new socket to each found address */ + for (p = addrs; p; p = p->ai_next) { + /* get the socket */ + sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sock < 0) { + perror("socket()"); + goto error; + } + + /* "address already in use" */ + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))) { + perror("setsockopt()"); + goto error; + } + + /* TCP keep-alive */ + if (conf.tcp_keepalive > 0) { + int ret = 0; +#ifdef linux + int tcp_ka_idl, tcp_ka_int, tcp_ka_cnt; + + tcp_ka_idl = 1; + tcp_ka_cnt = 3; + tcp_ka_int = conf->tcp_keepalive; + ret = setsockopt(sock, SOL_TCP, TCP_KEEPIDLE, &tcp_ka_idl, sizeof(tcp_ka_idl)) || + setsockopt(sock, SOL_TCP, TCP_KEEPINTVL, &tcp_ka_int, sizeof(tcp_ka_int)) || + setsockopt(sock, SOL_TCP, TCP_KEEPCNT, &tcp_ka_cnt, sizeof(tcp_ka_cnt)); +#endif + + if (ret || setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes))) + fprintf(stderr, "Notice: Unable to enable TCP keep-alive: %s\n", + strerror(errno)); + } + + /* required to get parallel v4 + v6 working */ + if (p->ai_family == AF_INET6 && + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)) < 0) { + perror("setsockopt()"); + goto error; + } + + /* bind */ + if (bind(sock, p->ai_addr, p->ai_addrlen) < 0) { + perror("bind()"); + goto error; + } + + /* listen */ + if (listen(sock, UH_LIMIT_CLIENTS) < 0) { + perror("listen()"); + goto error; + } + + fd_cloexec(sock); + + l = calloc(1, sizeof(*l)); + if (!l) + goto error; + + l->fd.fd = sock; + l->tls = tls; + list_add_tail(&l->list, &listeners); + + continue; + +error: + if (sock > 0) + close(sock); + } + + freeaddrinfo(addrs); + + return bound; +} diff --git a/main.c b/main.c new file mode 100644 index 0000000..37cba7f --- /dev/null +++ b/main.c @@ -0,0 +1,105 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "uhttpd.h" + + +static int run_server(void) +{ + uloop_init(); + uh_setup_listeners(); + uloop_run(); + + return 0; +} + +static void add_listener_arg(char *arg, bool tls) +{ + char *host = NULL; + char *port = arg; + char *s; + + s = strrchr(arg, ':'); + if (s) { + host = arg; + port = s + 1; + *s = 0; + } + uh_socket_bind(host, port, tls); +} + +static int usage(const char *name) +{ + fprintf(stderr, "Usage: %s -p \n", name); + return 1; +} + +static void init_defaults(void) +{ + conf.network_timeout = 30; + conf.http_keepalive = 0; /* fixme */ + + uh_index_add("index.html"); + uh_index_add("index.htm"); + uh_index_add("default.html"); + uh_index_add("default.htm"); +} + +int main(int argc, char **argv) +{ + int ch; + + init_defaults(); + signal(SIGPIPE, SIG_IGN); + + while ((ch = getopt(argc, argv, "sp:h:")) != -1) { + bool tls = false; + switch(ch) { + case 's': + tls = true; + case 'p': + add_listener_arg(optarg, tls); + break; + + case 'h': + /* docroot */ + if (!realpath(optarg, conf.docroot)) { + fprintf(stderr, "Error: Invalid directory %s: %s\n", + optarg, strerror(errno)); + exit(1); + } + break; + default: + return usage(argv[0]); + } + } + + return run_server(); +} diff --git a/uhttpd-mimetypes.h b/uhttpd-mimetypes.h new file mode 100644 index 0000000..7fd885c --- /dev/null +++ b/uhttpd-mimetypes.h @@ -0,0 +1,91 @@ +/* + * uhttpd - Tiny single-threaded httpd - MIME type definitions + * + * Copyright (C) 2010 Jo-Philipp Wich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _UHTTPD_MIMETYPES_ + +struct mimetype { + const char *extn; + const char *mime; +}; + +static struct mimetype uh_mime_types[] = { + + { "txt", "text/plain" }, + { "log", "text/plain" }, + { "js", "text/javascript" }, + { "css", "text/css" }, + { "htm", "text/html" }, + { "html", "text/html" }, + { "diff", "text/x-patch" }, + { "patch", "text/x-patch" }, + { "c", "text/x-csrc" }, + { "h", "text/x-chdr" }, + { "o", "text/x-object" }, + { "ko", "text/x-object" }, + + { "bmp", "image/bmp" }, + { "gif", "image/gif" }, + { "png", "image/png" }, + { "jpg", "image/jpeg" }, + { "jpeg", "image/jpeg" }, + { "svg", "image/svg+xml" }, + + { "zip", "application/zip" }, + { "pdf", "application/pdf" }, + { "xml", "application/xml" }, + { "xsl", "application/xml" }, + { "doc", "application/msword" }, + { "ppt", "application/vnd.ms-powerpoint" }, + { "xls", "application/vnd.ms-excel" }, + { "odt", "application/vnd.oasis.opendocument.text" }, + { "odp", "application/vnd.oasis.opendocument.presentation" }, + { "pl", "application/x-perl" }, + { "sh", "application/x-shellscript" }, + { "php", "application/x-php" }, + { "deb", "application/x-deb" }, + { "iso", "application/x-cd-image" }, + { "tar.gz", "application/x-compressed-tar" }, + { "tgz", "application/x-compressed-tar" }, + { "gz", "application/x-gzip" }, + { "tar.bz2", "application/x-bzip-compressed-tar" }, + { "tbz", "application/x-bzip-compressed-tar" }, + { "bz2", "application/x-bzip" }, + { "tar", "application/x-tar" }, + { "rar", "application/x-rar-compressed" }, + + { "mp3", "audio/mpeg" }, + { "ogg", "audio/x-vorbis+ogg" }, + { "wav", "audio/x-wav" }, + + { "mpg", "video/mpeg" }, + { "mpeg", "video/mpeg" }, + { "avi", "video/x-msvideo" }, + + { "README", "text/plain" }, + { "log", "text/plain" }, + { "cfg", "text/plain" }, + { "conf", "text/plain" }, + + { "pac", "application/x-ns-proxy-autoconfig" }, + { "wpad.dat", "application/x-ns-proxy-autoconfig" }, + + { NULL, NULL } +}; + +#endif + diff --git a/uhttpd.h b/uhttpd.h new file mode 100644 index 0000000..0cb8eb8 --- /dev/null +++ b/uhttpd.h @@ -0,0 +1,149 @@ +/* + * uhttpd - Tiny single-threaded httpd - Main header + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __UHTTPD_H +#define __UHTTPD_H + +#include +#include +#include + +#include +#include +#include +#include + +#include "utils.h" + +#define UH_LIMIT_CLIENTS 64 +#define UH_LIMIT_HEADERS 64 +#define UH_LIMIT_MSGHEAD 4096 + +struct config { + char docroot[PATH_MAX]; + char *realm; + char *file; + char *error_handler; + int no_symlinks; + int no_dirlists; + int network_timeout; + int rfc1918_filter; + int tcp_keepalive; + int max_requests; + int http_keepalive; +}; + +enum http_method { + UH_HTTP_MSG_GET, + UH_HTTP_MSG_POST, + UH_HTTP_MSG_HEAD, +}; + +enum http_version { + UH_HTTP_VER_0_9, + UH_HTTP_VER_1_0, + UH_HTTP_VER_1_1, +}; + +struct http_request { + enum http_method method; + enum http_version version; + int redirect_status; + char *url; + struct auth_realm *realm; +}; + +struct http_response { + int statuscode; + char *statusmsg; + char *headers[UH_LIMIT_HEADERS]; +}; + +enum client_state { + CLIENT_STATE_INIT, + CLIENT_STATE_HEADER, + CLIENT_STATE_DATA, + CLIENT_STATE_DONE, + CLIENT_STATE_CLOSE, +}; + +struct client { + struct list_head list; + int id; + + struct ustream *us; + struct ustream_fd sfd; +#ifdef HAVE_TLS + struct ustream_ssl stream_ssl; +#endif + struct uloop_fd rpipe; + struct uloop_fd wpipe; + struct uloop_process proc; + struct uloop_timeout timeout; + bool (*cb)(struct client *); + void *priv; + + enum client_state state; + + struct http_request request; + struct http_response response; + struct sockaddr_in6 servaddr; + struct sockaddr_in6 peeraddr; + + struct blob_buf hdr; + + void (*dispatch_write_cb)(struct client *cl); + void (*dispatch_free)(struct client *cl); + + union { + struct { + struct blob_attr **hdr; + int fd; + } file; + } data; +}; + +extern int n_clients; +extern struct config conf; + +void uh_index_add(const char *filename); + +void uh_accept_client(int fd); + +void uh_unblock_listeners(void); +void uh_setup_listeners(void); +int uh_socket_bind(const char *host, const char *port, bool tls); + +bool uh_use_chunked(struct client *cl); +void uh_chunk_write(struct client *cl, const void *data, int len); +void uh_chunk_vprintf(struct client *cl, const char *format, va_list arg); + +void __printf(2, 3) +uh_chunk_printf(struct client *cl, const char *format, ...); + +void uh_chunk_eof(struct client *cl); +void uh_request_done(struct client *cl); + +void uh_http_header(struct client *cl, int code, const char *summary); +void __printf(4, 5) +uh_client_error(struct client *cl, int code, const char *summary, const char *fmt, ...); + +void uh_handle_file_request(struct client *cl); + +#endif diff --git a/utils.c b/utils.c new file mode 100644 index 0000000..ec0b3aa --- /dev/null +++ b/utils.c @@ -0,0 +1,210 @@ +/* + * uhttpd - Tiny single-threaded httpd + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * Copyright (C) 2012 Felix Fietkau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "uhttpd.h" + +bool uh_use_chunked(struct client *cl) +{ + if (cl->request.version != UH_HTTP_VER_1_1) + return false; + + if (cl->request.method == UH_HTTP_MSG_HEAD) + return false; + + return true; +} + +void uh_chunk_write(struct client *cl, const void *data, int len) +{ + bool chunked = uh_use_chunked(cl); + + uloop_timeout_set(&cl->timeout, conf.network_timeout * 1000); + if (chunked) + ustream_printf(cl->us, "%X\r\n", len); + ustream_write(cl->us, data, len, true); + if (chunked) + ustream_printf(cl->us, "\r\n", len); +} + +void uh_chunk_vprintf(struct client *cl, const char *format, va_list arg) +{ + char buf[256]; + va_list arg2; + int len; + + uloop_timeout_set(&cl->timeout, conf.network_timeout * 1000); + if (!uh_use_chunked(cl)) { + ustream_vprintf(cl->us, format, arg); + return; + } + + va_copy(arg2, arg); + len = vsnprintf(buf, sizeof(buf), format, arg2); + va_end(arg2); + + ustream_printf(cl->us, "%X\r\n", len); + if (len < sizeof(buf)) + ustream_write(cl->us, buf, len, true); + else + ustream_vprintf(cl->us, format, arg); + ustream_printf(cl->us, "\r\n", len); +} + +void uh_chunk_printf(struct client *cl, const char *format, ...) +{ + va_list arg; + + va_start(arg, format); + uh_chunk_vprintf(cl, format, arg); + va_end(arg); +} + +void uh_chunk_eof(struct client *cl) +{ + if (!uh_use_chunked(cl)) + return; + + ustream_printf(cl->us, "0\r\n\r\n"); +} + +/* blen is the size of buf; slen is the length of src. The input-string need +** not be, and the output string will not be, null-terminated. Returns the +** length of the decoded string, -1 on buffer overflow, -2 on malformed string. */ +int uh_urldecode(char *buf, int blen, const char *src, int slen) +{ + int i; + int len = 0; + +#define hex(x) \ + (((x) <= '9') ? ((x) - '0') : \ + (((x) <= 'F') ? ((x) - 'A' + 10) : \ + ((x) - 'a' + 10))) + + for (i = 0; (i < slen) && (len < blen); i++) + { + if (src[i] == '%') + { + if (((i+2) < slen) && isxdigit(src[i+1]) && isxdigit(src[i+2])) + { + buf[len++] = (char)(16 * hex(src[i+1]) + hex(src[i+2])); + i += 2; + } + else + { + /* Encoding error: it's hard to think of a + ** scenario in which returning an incorrect + ** 'decoding' of the malformed string is + ** preferable to signaling an error condition. */ + #if 0 /* WORSE_IS_BETTER */ + buf[len++] = '%'; + #else + return -2; + #endif + } + } + else + { + buf[len++] = src[i]; + } + } + + return (i == slen) ? len : -1; +} + +/* blen is the size of buf; slen is the length of src. The input-string need +** not be, and the output string will not be, null-terminated. Returns the +** length of the encoded string, or -1 on error (buffer overflow) */ +int uh_urlencode(char *buf, int blen, const char *src, int slen) +{ + int i; + int len = 0; + const char hex[] = "0123456789abcdef"; + + for (i = 0; (i < slen) && (len < blen); i++) + { + if( isalnum(src[i]) || (src[i] == '-') || (src[i] == '_') || + (src[i] == '.') || (src[i] == '~') ) + { + buf[len++] = src[i]; + } + else if ((len+3) <= blen) + { + buf[len++] = '%'; + buf[len++] = hex[(src[i] >> 4) & 15]; + buf[len++] = hex[ src[i] & 15]; + } + else + { + len = -1; + break; + } + } + + return (i == slen) ? len : -1; +} + +int uh_b64decode(char *buf, int blen, const unsigned char *src, int slen) +{ + int i = 0; + int len = 0; + + unsigned int cin = 0; + unsigned int cout = 0; + + + for (i = 0; (i <= slen) && (src[i] != 0); i++) + { + cin = src[i]; + + if ((cin >= '0') && (cin <= '9')) + cin = cin - '0' + 52; + else if ((cin >= 'A') && (cin <= 'Z')) + cin = cin - 'A'; + else if ((cin >= 'a') && (cin <= 'z')) + cin = cin - 'a' + 26; + else if (cin == '+') + cin = 62; + else if (cin == '/') + cin = 63; + else if (cin == '=') + cin = 0; + else + continue; + + cout = (cout << 6) | cin; + + if ((i % 4) == 3) + { + if ((len + 3) < blen) + { + buf[len++] = (char)(cout >> 16); + buf[len++] = (char)(cout >> 8); + buf[len++] = (char)(cout); + } + else + { + break; + } + } + } + + buf[len++] = 0; + return len; +} diff --git a/utils.h b/utils.h new file mode 100644 index 0000000..4f74f5c --- /dev/null +++ b/utils.h @@ -0,0 +1,58 @@ +/* + * uhttpd - Tiny single-threaded httpd - Utility header + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _UHTTPD_UTILS_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define min(x, y) (((x) < (y)) ? (x) : (y)) +#define max(x, y) (((x) > (y)) ? (x) : (y)) + +#define array_size(x) \ + (sizeof(x) / sizeof(x[0])) + +#define fd_cloexec(fd) \ + fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC) + +#ifdef __APPLE__ +static inline void clearenv(void) +{ + extern char **environ; + environ = NULL; +} +#endif + +#ifdef __GNUC__ +#define __printf(a, b) __attribute__((format(printf, a, b))) +#else +#define __printf(a, b) +#endif + +int uh_urldecode(char *buf, int blen, const char *src, int slen); +int uh_urlencode(char *buf, int blen, const char *src, int slen); +int uh_b64decode(char *buf, int blen, const unsigned char *src, int slen); +#endif -- 2.25.1