diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a9722db --- /dev/null +++ b/.clang-format @@ -0,0 +1,63 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +AccessModifierOffset: -4 + +AlignAfterOpenBracket: DontAlign +AlignEscapedNewlinesLeft: true +# AlignOperands: true +AlignTrailingComments: true + +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false + +AlwaysBreakAfterDefinitionReturnType: All +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false + +# BinPackArguments: false +# BinPackParameters: true + +BreakBeforeBinaryOperators: false +BreakBeforeBraces: Custom +BraceWrapping: { AfterFunction: true } +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: true + +ColumnLimit: 80 + +ContinuationIndentWidth: 4 + +DerivePointerAlignment: false #XXX +DisableFormat: false +ExperimentalAutoDetectBinPacking: false #XXX +ForEachMacros: [ LIST_FOREACH, SIMPLEQ_FOREACH, CIRCLEQ_FOREACH, TAILQ_FOREACH, TAILQ_FOREACH_REVERSE, HT_FOREACH ] + +IndentCaseLabels: false +IndentFunctionDeclarationAfterType: false +IndentWidth: 4 +IndentWrappedFunctionNames: false + +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 + +PointerAlignment: Right #XXX + +# SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +Standard: Cpp03 +TabWidth: 4 +UseTab: Always +SortIncludes: false +... diff --git a/.gitignore b/.gitignore index 64a2fb1..a982ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ /examples_R8/R8_echo_server /examples_R9/R9_multilookup /examples_R9/R9_dns_server +/examples_R10/R10_simple_server +/examples_R10/R10_static_server /tmpcode* diff --git a/LibeventBook.txt b/LibeventBook.txt index 69517a2..2d9d3b7 100644 --- a/LibeventBook.txt +++ b/LibeventBook.txt @@ -36,6 +36,5 @@ include::Ref8_listener.txt[] include::Ref9_dns.txt[] - - +include::Ref10_http_server.txt[] diff --git a/Makefile b/Makefile index 5b59936..de4b786 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ GENERATED_CHAPTERS= \ Ref7_evbuffer.html \ Ref8_listener.html \ Ref9_dns.html \ + Ref10_http_server.html \ license_bsd.html GENERATED_HTML = $(GENERATED_METAFILES) $(GENERATED_CHAPTERS) @@ -42,6 +43,7 @@ examples: cd examples_R6a && $(MAKE) cd examples_R8 && $(MAKE) cd examples_R9 && $(MAKE) + cd examples_R10 && $(MAKE) inline_examples: ./bin/build_examples.py *_*.txt @@ -66,6 +68,7 @@ Ref6a_advanced_bufferevent.html: examples_R6a/*.c license.txt Ref7_evbuffer.html: license.txt Ref8_listener.html: examples_R8/*.c license.txt Ref9_dns.html: examples_R9/*.c license.txt +Ref10_http_server.html: examples_R10/*.c license.txt clean: rm -f *~ diff --git a/Ref10_http_server.txt b/Ref10_http_server.txt new file mode 100644 index 0000000..5637c88 --- /dev/null +++ b/Ref10_http_server.txt @@ -0,0 +1,57 @@ +:docinfo: + +Using the built-in HTTP server +============================== + +include::license.txt[] + +:language: C + +The plain network-based Libevent interface is useful if you want to build native +applications, but it is increasingly common to develop an application based +around the HTTP protocol and a web page that loads, or more commonly dynamically +reloads, information. + +To use the Libevent service, you use the same basic structure as already +described for the main network event model, but instead of having to handle the +network interfacing, the HTTP wrapper handles that for you. This turns the +entire process into the four function calls (initialize, start HTTP server, set +HTTP callback function, and enter event loop), plus the contents of the callback +function that will send data back. A very simple example is provided in the listing: + + +.Example: A basic HTTP server +[code,C] +------ +include::examples_R10/R10_simple_server.c[] +------ + +Given the previous example, the basics of the code here should be relatively +self-explanatory. The main elements are the evhttp_set_gencb() function, which +sets the callback function to be used when an HTTP request is received, and the +generic_request_handler() callback function itself, which populates the response +buffer with a simple message to show success. + +The HTTP wrapper provides a wealth of different functionality. For example, +there is a request parser that will extract the query arguments from a typical +request (as you would use in a HTTP request), and you can also set different +handlers to be triggered within different requested paths. + +Let's extend this example act Libevent as Nginx-like server for static content: + +.Example: A static HTTP server implementation +[code,C] +------ +include::examples_R10/R10_static_server.c[] +------ + +As you can see here we've replaced generic_request_handler() by specific +send_file_to_user() handler which processes incoming request: + +* First it checks if HTTP command is equal to `GET` or `HEAD` + +* Then it parses request URI to extract request path and determine file path we + should handle by couple evhttp_request_get_uri()/evhttp_uri_parse() functions + +* After that it decoded URI string from something like `folder%2Fmy%20doc.txt` + to plain `folder/my doc.txt` diff --git a/TOC.txt b/TOC.txt index 1501cc7..827539b 100644 --- a/TOC.txt +++ b/TOC.txt @@ -24,6 +24,7 @@ A Libevent Reference Manual - link:Ref7_evbuffer.html[R7: Evbuffers: utility functionality for buffered IO] - link:Ref8_listener.html[R8: Connection listeners: accepting TCP connections] - link:Ref9_dns.html[R9: DNS for Libevent] +- link:Ref10_http_server.html[R10: HTTP server] include::license.txt[] diff --git a/examples_R10/Makefile b/examples_R10/Makefile new file mode 100644 index 0000000..899d127 --- /dev/null +++ b/examples_R10/Makefile @@ -0,0 +1,23 @@ + +CC=gcc +CFLAGS=-g -Wall $(LEBOOK_CFLAGS) + +EXAMPLE_BINARIES=R10_simple_server R10_static_server + +all: examples + +examples: $(EXAMPLE_BINARIES) + +R10_simple_server: R10_simple_server.o + $(CC) $(CFLAGS) R10_simple_server.o -o R10_simple_server -levent + +R10_static_server: R10_static_server.o + $(CC) $(CFLAGS) R10_static_server.o -o R10_static_server -levent + +.c.o: + $(CC) $(CFLAGS) -c $< + +clean: + rm -f *~ + rm -f *.o + rm -f $(EXAMPLE_BINARIES) diff --git a/examples_R10/R10_simple_server.c b/examples_R10/R10_simple_server.c new file mode 100644 index 0000000..31a8c89 --- /dev/null +++ b/examples_R10/R10_simple_server.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +static void +generic_request_handler(struct evhttp_request *req, void *ctx) +{ + struct evbuffer *reply = evbuffer_new(); + + evbuffer_add_printf(reply, "It works!"); + evhttp_send_reply(req, HTTP_OK, NULL, reply); + evbuffer_free(reply); +} + +static void +signal_cb(evutil_socket_t fd, short event, void *arg) +{ + printf("%s signal received\n", strsignal(fd)); + event_base_loopbreak(arg); +} + +int +main() +{ + ev_uint16_t http_port = 8080; + char *http_addr = "0.0.0.0"; + struct event_base *base; + struct evhttp *http_server; + struct event *sig_int; + + base = event_base_new(); + + http_server = evhttp_new(base); + evhttp_bind_socket(http_server, http_addr, http_port); + evhttp_set_gencb(http_server, generic_request_handler, NULL); + + sig_int = evsignal_new(base, SIGINT, signal_cb, base); + event_add(sig_int, NULL); + + printf("Listening requests on http://%s:%d\n", http_addr, http_port); + + event_base_dispatch(base); + + evhttp_free(http_server); + event_free(sig_int); + event_base_free(base); +} diff --git a/examples_R10/R10_static_server.c b/examples_R10/R10_static_server.c new file mode 100644 index 0000000..a5e1f65 --- /dev/null +++ b/examples_R10/R10_static_server.c @@ -0,0 +1,330 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define BOOTSTRAP_CDN "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist" +#define BOOTSTRAP_JS BOOTSTRAP_CDN "/js" +#define BOOTSTRAP_CSS BOOTSTRAP_CDN "/css" + +static const struct table_entry { + const char *extension; + const char *content_type; +} content_type_table[] = { + {"txt", "text/plain"}, + {"c", "text/plain"}, + {"h", "text/plain"}, + {"html", "text/html"}, + {"htm", "text/htm"}, + {"css", "text/css"}, + {"gif", "image/gif"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"png", "image/png"}, + {"pdf", "application/pdf"}, + {"ps", "application/postscript"}, + {NULL, NULL}, +}; + +static void +add_content_length(struct evhttp_request *req, unsigned len) +{ + char buf[128]; + + snprintf(buf, sizeof(buf), "%u", len); + evhttp_add_header( + evhttp_request_get_output_headers(req), "Content-Length", buf); +} + +#if defined(WIN32) +#define DIR_SEPARATOR '\\' +#else +#define DIR_SEPARATOR '/' +#endif + +static void +path_join(char *destination, const char *path1, const char *path2) +{ + if (path1 && *path1) { + ssize_t len = strlen(path1); + strcpy(destination, path1); + + if (destination[len - 1] == DIR_SEPARATOR) { + if (path2 && *path2) { + strcpy(destination + len, + (*path2 == DIR_SEPARATOR) ? (path2 + 1) : path2); + } + } else { + if (path2 && *path2) { + if (*path2 == DIR_SEPARATOR) + strcpy(destination + len, path2); + else { + destination[len] = DIR_SEPARATOR; + strcpy(destination + len + 1, path2); + } + } + } + } else if (path2 && *path2) + strcpy(destination, path2); + else + destination[0] = '\0'; +} + +/* Try to guess the content type of "path" */ +static const char * +guess_content_type(const char *path) +{ + const char *last_period, *extension; + const struct table_entry *ent; + last_period = strrchr(path, '.'); + if (!last_period || strchr(last_period, '/')) + goto not_found; /* no exension */ + extension = last_period + 1; + for (ent = &content_type_table[0]; ent->extension; ++ent) { + if (!evutil_ascii_strcasecmp(ent->extension, extension)) + return ent->content_type; + } + +not_found: + return "application/stream"; +} + +static void +send_file_to_user(struct evhttp_request *req, void *arg) +{ + struct evbuffer *evb = NULL; + struct evhttp_uri *decoded = NULL; + struct stat st; + int fd = -1; + const char *static_dir = "."; + + enum evhttp_cmd_type cmd = evhttp_request_get_command(req); + if (cmd != EVHTTP_REQ_GET && cmd != EVHTTP_REQ_HEAD) { + return; + } + + /* Decode the URI */ + decoded = evhttp_uri_parse(evhttp_request_get_uri(req)); + if (!decoded) { + evhttp_send_error(req, HTTP_BADREQUEST, 0); + return; + } + + /* Let's see what path the user asked for. */ + const char *path = evhttp_uri_get_path(decoded); + if (!path) + path = "/"; + + /* We need to decode it, to see what path the user really wanted. */ + char *decoded_path = evhttp_uridecode(path, 0, NULL); + if (decoded_path == NULL) + goto err; + + /* Don't allow any ".."s in the path, to avoid exposing stuff outside + * of the docroot. This test is both overzealous and underzealous: + * it forbids aceptable paths like "/this/one..here", but it doesn't + * do anything to prevent symlink following." */ + if (strstr(decoded_path, "..")) + goto err; + + char whole_path[PATH_MAX] = {0}; + const char *type = NULL; + path_join(whole_path, static_dir, decoded_path); + char *real_file = realpath(whole_path, NULL); + if (real_file) { + strncpy(whole_path, real_file, sizeof(whole_path)); + free(real_file); + } else { + /* check also if is there gz-ready static version */ + type = guess_content_type(whole_path); + + char gz_path[PATH_MAX + 3] = {0}; + snprintf(gz_path, sizeof(gz_path), "%s.gz", whole_path); + char *real_file = realpath(gz_path, NULL); + if (real_file) { + evhttp_add_header(evhttp_request_get_output_headers(req), + "Content-Encoding", "gzip"); + strncpy(whole_path, real_file, sizeof(whole_path)); + free(real_file); + } else { + fprintf(stderr, "File '%s' not found\n", whole_path); + evhttp_send_error(req, HTTP_NOTFOUND, NULL); + goto done; + } + } + + if (stat(whole_path, &st) < 0) { + goto err; + } + + if ((evb = evbuffer_new()) == NULL) { + evhttp_send_error(req, HTTP_INTERNAL, 0); + goto cleanup; + } + + bool dir_mode = false; + + if (S_ISDIR(st.st_mode)) { + /* Check if there is index.html file */ + char index_file[PATH_MAX + 11]; + snprintf(index_file, sizeof(index_file), "%s/index.html", whole_path); + + if (stat(index_file, &st) < 0) + dir_mode = true; + else + strcpy(whole_path, index_file); + } + + if (dir_mode) { + DIR *d; + struct dirent *ent; + + const char *trailing_slash = ""; + + if (!strlen(path) || path[strlen(path) - 1] != '/') + trailing_slash = "/"; + if (!(d = opendir(whole_path))) { + goto err; + } + + evbuffer_add_printf(evb, + "\n" + "" + "\n" + "\n" + "%s\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
\n" + "
\n" + "

%s

\n" + "
    \n", + decoded_path, /* XXX html-escape this. */ + path, /* XXX html-escape this? */ + trailing_slash, decoded_path /* XXX html-escape this */); + while ((ent = readdir(d))) { + const char *name = ent->d_name; + // ignore '.' directory entries + if (strcmp(name, ".") == 0) + continue; + // show '..' only for subdirs + if (strcmp(path, "/") == 0 && strcmp(name, "..") == 0) + continue; + evbuffer_add_printf(evb, "
  • %s\n", name, + name); /* XXX escape this */ + } + evbuffer_add_printf(evb, "
" + "
\n" + "
\n" + "\n" + "" + "\n"); + closedir(d); + + add_content_length(req, evbuffer_get_length(evb)); + if (cmd == EVHTTP_REQ_HEAD) + evbuffer_drain(evb, evbuffer_get_length(evb)); + evhttp_add_header(evhttp_request_get_output_headers(req), + "Content-Type", "text/html"); + } else { + /* Otherwise it's a file; add it to the buffer to get + * sent via sendfile */ + if (type == NULL) + type = guess_content_type(whole_path); + evhttp_add_header( + evhttp_request_get_output_headers(req), "Content-Type", type); + + if (st.st_size != 0) { + if ((fd = open(whole_path, O_RDONLY)) == -1) { + if (errno == ENOENT) { + fprintf(stderr, "File '%s' not found\n", whole_path); + evhttp_send_error(req, HTTP_NOTFOUND, NULL); + } else { + evhttp_send_error(req, HTTP_INTERNAL, NULL); + } + goto done; + } + if (cmd != EVHTTP_REQ_HEAD) { + if (evbuffer_add_file(evb, fd, 0, st.st_size) != 0) { + evhttp_send_error(req, HTTP_INTERNAL, NULL); + goto cleanup; + } + } + } + add_content_length(req, st.st_size); + } + evhttp_send_reply(req, HTTP_OK, "OK", evb); + + goto done; + +err: + evhttp_send_error(req, HTTP_NOTFOUND, NULL); +cleanup: + if (fd >= 0) + close(fd); + +done: + if (decoded) + evhttp_uri_free(decoded); + if (decoded_path) + free(decoded_path); + if (evb) + evbuffer_free(evb); +} + +static void +signal_cb(evutil_socket_t fd, short event, void *arg) +{ + printf("%s signal received\n", strsignal(fd)); + event_base_loopbreak(arg); +} + +int +main() +{ + ev_uint16_t http_port = 8080; + char *http_addr = "0.0.0.0"; + struct event_base *base; + struct evhttp *http_server; + struct event *sig_int; + + base = event_base_new(); + + http_server = evhttp_new(base); + evhttp_bind_socket(http_server, http_addr, http_port); + evhttp_set_gencb(http_server, send_file_to_user, NULL); + + sig_int = evsignal_new(base, SIGINT, signal_cb, base); + event_add(sig_int, NULL); + + printf("Listening requests on http://%s:%d\n", http_addr, http_port); + + event_base_dispatch(base); + + evhttp_free(http_server); + event_free(sig_int); + event_base_free(base); +}