add a manpage and README, and update the code to reflect the light changes - libgcgi - REST library for Gopher HTML git clone git://bitreich.org/libgcgi git://hg6vgqziawt5s4dj.onion/libgcgi DIR Log DIR Files DIR Refs DIR Tags DIR README DIR LICENSE --- DIR commit f1f14c75ca3477d51d1a4e09b917a5e5f869e672 DIR parent 052f666afd7390d53ec4b3ad91882e7e76b7a49f HTML Author: Josuah Demangeon <me@josuah.net> Date: Tue, 2 Aug 2022 13:20:15 +0200 add a manpage and README, and update the code to reflect the light changes Diffstat: M Makefile | 3 +++ A README | 199 +++++++++++++++++++++++++++++++ M db/vars | 2 +- D gph/404.gph | 1 - A gph/page_not_found.gph | 1 + M index.c | 8 ++++---- A libgcgi.3 | 339 +++++++++++++++++++++++++++++++ M libgcgi.c | 28 ++++++++++++++++++++++------ M libgcgi.h | 4 +--- 9 files changed, 570 insertions(+), 15 deletions(-) --- DIR diff --git a/Makefile b/Makefile @@ -6,5 +6,8 @@ all: index.cgi clean: rm -f *.o index.cgi +README: libgcgi.3 + mandoc -Tutf8 libgcgi.3 | col -b | sed '1h; $$g' >$@ + index.cgi: index.c libgcgi.c libgcgi.h ${CC} ${LDFLAGS} ${CFLAGS} -o $@ index.c libgcgi.c DIR diff --git a/README b/README @@ -0,0 +1,199 @@ +LIBGCGI(3) Library Functions Manual LIBGCGI(3) + +NAME + gcgi_handle_request, gcgi_fatal, gcgi_template, gcgi_set_var, + gcgi_get_var, gcgi_free_var_list, gcgi_read_var_list, + gcgi_write_var_list, gcgi_gopher_search, gcgi_gopher_path, + gcgi_gopher_query, gcgi_gopher_host, gcgi_gopher_port, REST library for + Gopher + +SYNOPSIS + #include <libgcgi.h> + + void + gcgi_handle_request(struct gcgi_handler h[], char **argv, int argc); + + void + gcgi_fatal(char *fmt, ...); + + void + gcgi_template(char const *path, struct gcgi_var_list *vars); + + void + gcgi_set_var(struct gcgi_var_list *vars, char *key, char *val); + + char * + gcgi_get_var(struct gcgi_var_list *vars, char *key); + + void + gcgi_free_var_list(struct gcgi_var_list *vars); + + void + gcgi_read_var_list(struct gcgi_var_list *vars, char *path); + + int + gcgi_write_var_list(struct gcgi_var_list *vars, char *path); + + char *gcgi_gopher_search + char *gcgi_gopher_path + char *gcgi_gopher_host + char *gcgi_gopher_port + struct gcgi_var_list gcgi_gopher_query + +DESCRIPTION + Request Handling + The central element of the library is an array of structures, using + appropriate handler depending on the query path. + + struct gcgi_handler { + char const *glob; + void (*fn)(char **matches); + }; + + The glob is a string against which the path (everything in the query + before the ?) will be matched against. + + The fn function pointer will be called, with an array of matches passed + as argument. There are as many matches populated as there are * in + glob. + + void gcgi_handle_request(struct gcgi_handler h[], int argc, char **argv) + Given an array of handlers h, call the first function pointer + that matches. argc and argv should be set to the program ones to + extract the arguments given by geomyidae(8). The h struct is an + array of struct gcgi_handler: + + Content Generation + According to geomyidae(8) behavior, the output format will be: + a raw gophermap if the binary is index.cgi, + a geomyidae(8) gph format if the binary is index.dcgi. + + void gcgi_fatal(char *fmt, ...) + Prints an error message formatted by fmt and exit(3) the program + with status 1. + + void gcgi_template(char const *path, struct gcgi_var_list *vars) + Format the template at path replacing every occurence of + {{key}} by the matching value by searching in vars. + + void gcgi_print_gophermap(char type, char *desc, char *path, char *host, + char *port) + Print a gophermap entry line with type, desc, path, host, port to + be set to the chosen value as described in RFC 1436. Both host + and port are NULL, default values will be used. + + + void gcgi_print_gph(char type, char *desc, char *path, char *host, char + *port) + Print a gph entry line with type, desc, path, host, port to be + set to the chosen value as described in geomyidae(8) manual page. + If host or port are NULL, default values will be used. + + Variable List Handling + A common data format is used for handling lists of variables: + For parsing a simple text-based database format and writing it back. + For storing the parsed query string in gcgi_gopher_query. + For passing variables to expand in the templates. + + void gcgi_set_var(struct gcgi_var_list *vars, char *key, char *val) + Overwrite with val the value of a variable matching key of vars. + The key and val buffers are not duplicated, and must remain valid + at all time they need to be accessible, such as through + gcgi_get_var(). + + char * gcgi_get_var(struct gcgi_var_list *vars, char *key) + Get the value of the variable of vars matching key or NULL if + none match. + + void gcgi_free_var_list(struct gcgi_var_list *vars) + Free memory used by a list of variable. This only frees the + memory allocated by this library. + + void gcgi_read_var_list(struct gcgi_var_list *vars, char *path) + Store all variables from path onto variables in vars. The file + format is similar to RFC822 messages or HTTP headers: + One line per variable, with a key=value format. + The key is everything at the beginning of the line until the + occurence of :. + The value is everything after : . + After the list of variables, an empty line declares the body + of the message, which continues until the end and is stored in + a text key. + + int gcgi_write_var_list(struct gcgi_var_list *vars, char *path) + Encode the variable list vars into a new file at path. A + temporary file will be created in the meantime, and the + replacement will be atomic so that no partial write can occur. + The text special key will be turned into the body of the + message after an empty line instead of a variable on its own + line. + + Global Variables + These variables are filled with the components of the query. They will + only be valid after handle_request() is called. + + char *gcgi_gopher_search + From argv[1], this is the search string, passed after a tab in + the gopher protocol for item type 7. + + char *gcgi_gopher_path + From argv[2], this is the query path. It is the full query + without the search string and with the query string removed. + + struct gcgi_var_list gcgi_gopher_query + From argv[2], this is the query string stored as a key-value + gcgi_var_list. It is extracted from the part of the query after + the ?, usually formated as + ?key1=value1&key2=value2&key3=value3 + + char *gcgi_gopher_host + From argv[3], this is the current host name configured in + geomyidae(8). It is what to use as a host in links printed + out. + + char *gcgi_gopher_port + From argv[4], this is the current port number configured in + geomyidae(8). It is what to use as a port in links printed + out. + +EXAMPLES + #include "libgcgi.h" + + /* implementation of each handler here */ + + static struct gcgi_handler handlers[] = { + { "/", page_home }, + { "/song", page_song_list }, + { "/song/*", page_song_item }, + { "*", page_not_found }, + { NULL, NULL }, + }; + + int + main(int argc, char **argv) + { + /* privilege dropping, chroot and/or syscall restriction here */ + + gcgi_handle_request(handlers, argv, argc); + return 0; + } + +ENVIRONMENT VARIABLES + libgcgi does not use environment variable, but the application code can + make use of them. The environment variables applied to geomyidae(8) will + be inherited and accessible. + +BUGS + To debug libgcgi, it is possible to call it on a command line, which will + show all logging and error messages displayed on stderr: + + $ ./index.cgi "" "/song/never-bored-of-adventure?lyrics=1&comments=1" "" "" + +CAVEATS + The Gopher protocol is not designed for file upload. A dedicated file + upload protocol such as SFTP or FTP may be used instead. + + The Gopher protocol is not designed for dynamic scripting. A dedicated + remote interface protocol such as SSH or telnet may be used instead. + +LIBGCGI(3) Library Functions Manual LIBGCGI(3) DIR diff --git a/db/vars b/db/vars @@ -1 +1 @@ -Variable-From-Db: Lucky 777 Hat +name: world DIR diff --git a/gph/404.gph b/gph/404.gph @@ -1 +0,0 @@ -Hello world! DIR diff --git a/gph/page_not_found.gph b/gph/page_not_found.gph @@ -0,0 +1 @@ +Hello {{name}}! DIR diff --git a/index.c b/index.c @@ -10,7 +10,7 @@ #endif static void -error_404(char **matches) +error_page_not_found(char **matches) { struct gcgi_var_list vars = {0}; char *var; @@ -21,12 +21,12 @@ error_404(char **matches) if ((var = gcgi_get_var(&gcgi_gopher_query, "var")) != NULL) printf("I got the $var though! -> '%s'\n", var); - gcgi_template("gph/404.gph", &vars); + gcgi_template("gph/error_page_not_found.gph", &vars); } static struct gcgi_handler handlers[] = { - { "*", error_404 }, - { NULL, NULL }, + { "*", error_page_not_found }, + { NULL, NULL }, }; int DIR diff --git a/libgcgi.3 b/libgcgi.3 @@ -0,0 +1,339 @@ +.Dd $Mdocdate: August 01 2022 $ +.Dt LIBGCGI 3 +.Os +. +. +.Sh NAME +. +.Nm gcgi_handle_request , +.Nm gcgi_fatal , +.Nm gcgi_template , +.Nm gcgi_set_var , +.Nm gcgi_get_var , +.Nm gcgi_free_var_list , +.Nm gcgi_read_var_list , +.Nm gcgi_write_var_list , +.Nm gcgi_gopher_search , +.Nm gcgi_gopher_path , +.Nm gcgi_gopher_query , +.Nm gcgi_gopher_host , +.Nm gcgi_gopher_port , +.Nd REST library for Gopher +. +. +.Sh SYNOPSIS +. +.In libgcgi.h +. +.Ft "void" Fn gcgi_handle_request "struct gcgi_handler h[]" "char **argv" "int argc" +.Ft "void" Fn gcgi_fatal "char *fmt" "..." +.Ft "void" Fn gcgi_template "char const *path" "struct gcgi_var_list *vars" +.Ft "void" Fn gcgi_set_var "struct gcgi_var_list *vars" "char *key" "char *val" +.Ft "char *" Fn gcgi_get_var "struct gcgi_var_list *vars" "char *key" +.Ft "void" Fn gcgi_free_var_list "struct gcgi_var_list *vars" +.Ft "void" Fn gcgi_read_var_list "struct gcgi_var_list *vars" "char *path" +.Ft "int" Fn gcgi_write_var_list "struct gcgi_var_list *vars" "char *path" +.Vt char *gcgi_gopher_search +.Vt char *gcgi_gopher_path +.Vt char *gcgi_gopher_host +.Vt char *gcgi_gopher_port +.Vt struct gcgi_var_list gcgi_gopher_query +. +. +.Sh DESCRIPTION +. +. +.Ss Request Handling +. +The central element of the library is an array of structures, +using appropriate handler depending on the query path. +.Pp +.Bd -literal +struct gcgi_handler { + char const *glob; + void (*fn)(char **matches); +}; +.Ed +. +.Pp +The +.Vt glob +is a string against which the path (everything in the query before the +.Dq ? ) +will be matched against. +.Pp +The +.Vt fn +function pointer will be called, with an array of matches passed as argument. +There are as many matches populated as there are +.Dq "*" +in +.Vt glob . +. +.Pp +.Bl -tag +. +.It Ft "void" Fn gcgi_handle_request "struct gcgi_handler h[]" "int argc" "char **argv" +Given an array of handlers +.Fa h , +call the first function pointer that matches. +.Fa argc +and +.Fa argv +should be set to the program ones to extract the arguments given by +.Xr geomyidae 8 . +The +.Fa h +struct is an array of +.Vt struct gcgi_handler : +. +.El +. +. +.Ss Content Generation +. +According to +.Xr geomyidae 8 +behavior, the output format will be: +.Bl -bullet -compact -width x +. +.It +a raw gophermap if the binary is +.Dq index.cgi , +.It +a +.Xr geomyidae 8 +.Sq gph +format if the binary is +.Dq index.dcgi . +.El +. +.Pp +.Bl -tag +. +.It Ft "void" Fn gcgi_fatal "char *fmt" "..." +Prints an error message formatted by +.Fa fmt +and +.Xr exit 3 +the program with status 1. +. +.It Ft "void" Fn gcgi_template "char const *path" "struct gcgi_var_list *vars" +Format the template at +.Fa path +replacing every occurence of +.Dq {{key}} +by the matching value by searching in +.Fa vars . +. +.It Vt void Fn gcgi_print_gophermap "char type" "char *desc" "char *path" "char *host" "char *port" +Print a gophermap entry line with +.Fa type , +.Fa desc , +.Fa path , +.Fa host , +.Fa port +to be set to the chosen value as described in RFC 1436. +Both +.Fa host +and +.Fa port +are NULL, default values will be used. + +.It Ft void Fn gcgi_print_gph "char type" "char *desc" "char *path" "char *host" "char *port" +Print a gph entry line with +.Fa type , +.Fa desc , +.Fa path , +.Fa host , +.Fa port +to be set to the chosen value as described in +.Xr geomyidae 8 +manual page. +If +.Fa host +or +.Fa port +are NULL, default values will be used. +. +.El +. +. +.Ss Variable List Handling +. +A common data format is used for handling lists of variables: +.Bl -bullet -compact -width x +.It +For parsing a simple text-based database format and writing it back. +.It +For storing the parsed query string in +.Vt gcgi_gopher_query . +.It +For passing variables to expand in the templates. +.El +. +.Pp +.Bl -tag +. +.It Ft "void" Fn gcgi_set_var "struct gcgi_var_list *vars" "char *key" "char *val" +Overwrite with +.Fa val +the value of a variable matching +.Fa key +of +.Fa vars . +The +.Fa key +and +.Fa val +buffers are not duplicated, and must remain valid at all time they need to be +accessible, such as through +.Fn gcgi_get_var . +. +.It Ft "char *" Fn gcgi_get_var "struct gcgi_var_list *vars" "char *key" +Get the value of the variable of +.Fa vars +matching +.Fa key +or NULL if none match. +. +.It Ft "void" Fn gcgi_free_var_list "struct gcgi_var_list *vars" +Free memory used by a list of variable. +This only frees the memory allocated by this library. +. +.It Ft "void" Fn gcgi_read_var_list "struct gcgi_var_list *vars" "char *path" +Store all variables from +.Fa path +onto variables in +.Fa vars . +The file format is similar to RFC822 messages or HTTP headers: +.Bl -bullet -compact -width x +.It +One line per variable, with a key=value format. +.It +The key is everything at the beginning of the line until the occurence of +.Dq ":" . +.It +The value is everything after +.Dq ": " . +.It +After the list of variables, an empty line declares the body of the message, +which continues until the end and is stored in a +.Dq text +key. +.El +. +.It Ft "int" Fn gcgi_write_var_list "struct gcgi_var_list *vars" "char *path" +Encode the variable list +.Fa vars +into a new file at +.Fa path . +A temporary file will be created in the meantime, +and the replacement will be atomic so that no partial write can occur. +The +.Dq text +special key will be turned into the body of the message after an empty line +instead of a variable on its own line. +. +.El +. +. +.Ss Global Variables +. +These variables are filled with the components of the query. +They will only be valid after +.Fn handle_request +is called. +. +.Pp +.Bl -tag +. +.It Vt char *gcgi_gopher_search +From argv[1], this is the search string, passed after a tab in +the gopher protocol for item type +.Dq 7 . +. +.It Vt char *gcgi_gopher_path +From argv[2], this is the query path. +It is the full query without the search string and with the query string removed. +. +.It Vt struct gcgi_var_list gcgi_gopher_query +From argv[2], this is the query string stored as a key-value +.Vt gcgi_var_list . +It is extracted from the part of the query after the +.Dq ? , +usually formated as +.Dq ?key1=value1&key2=value2&key3=value3 +. +.It Vt char *gcgi_gopher_host +From argv[3], this is the current host name configured in +.Xr geomyidae 8 . +It is what to use as a +.Sq host +in links printed out. +. +.It Vt char *gcgi_gopher_port +From argv[4], this is the current port number configured in +.Xr geomyidae 8 . +It is what to use as a +.Sq port +in links printed out. +. +.El +. +. +.Sh EXAMPLES +. +. +.Bd -literal +#include "libgcgi.h" + +/* implementation of each handler here */ + +static struct gcgi_handler handlers[] = { + { "/", page_home }, + { "/song", page_song_list }, + { "/song/*", page_song_item }, + { "*", page_not_found }, + { NULL, NULL }, +}; + +int +main(int argc, char **argv) +{ + /* privilege dropping, chroot and/or syscall restriction here */ + + gcgi_handle_request(handlers, argv, argc); + return 0; +} +.Ed +. +. +.Sh ENVIRONMENT VARIABLES +. +.Nm libgcgi +does not use environment variable, but the application code can make use of them. +The environment variables applied to +.Xr geomyidae 8 +will be inherited and accessible. +. +. +.Sh BUGS +. +To debug +.Nm libgcgi , +it is possible to call it on a command line, which will show all logging and error messages displayed on stderr: +. +.Bd -literal +$ ./index.cgi "" "/song/never-bored-of-adventure?lyrics=1&comments=1" "" "" +.Ed +. +. +.Sh CAVEATS +. +The Gopher protocol is not designed for file upload. +A dedicated file upload protocol such as SFTP or FTP may be used instead. +. +.Pp +The Gopher protocol is not designed for dynamic scripting. +A dedicated remote interface protocol such as SSH or telnet may be used instead. DIR diff --git a/libgcgi.c b/libgcgi.c @@ -70,7 +70,7 @@ gcgi_cmp_var(const void *v1, const void *v2) return strcasecmp(((struct gcgi_var *)v1)->key, ((struct gcgi_var *)v2)->key); } -void +static void gcgi_add_var(struct gcgi_var_list *vars, char *key, char *val) { void *mem; @@ -83,7 +83,7 @@ gcgi_add_var(struct gcgi_var_list *vars, char *key, char *val) vars->list[vars->len-1].val = val; } -void +static void gcgi_sort_var_list(struct gcgi_var_list *vars) { qsort(vars->list, vars->len, sizeof *vars->list, gcgi_cmp_var); @@ -138,8 +138,7 @@ gcgi_read_var_list(struct gcgi_var_list *vars, char *path) void gcgi_free_var_list(struct gcgi_var_list *vars) { - if (vars->buf != NULL) - free(vars->buf); + free(vars->buf); free(vars->list); } @@ -159,7 +158,7 @@ gcgi_write_var_list(struct gcgi_var_list *vars, char *dst) gcgi_fatal("opening '%s' for writing", path); for (v = vars->list, n = vars->len; n > 0; v++, n--) { - if (strcasecmp(v->key, "Text") == 0) { + if (strcasecmp(v->key, "text") == 0) { text = text ? text : v->val; continue; } @@ -203,7 +202,6 @@ gcgi_decode_url(struct gcgi_var_list *vars, char *s) char *tok, *eq; while ((tok = strsep(&s, "&"))) { - //gcgi_decode_hex(tok); if ((eq = strchr(tok, '=')) == NULL) continue; *eq = '\0'; @@ -256,6 +254,24 @@ gcgi_next_var(char *head, char **tail) } void +gcgi_print_gophermap(char type, char *desc, char *path, char *host, char *port) +{ + assert(type >= 0x30); + printf("%c%s\t%s\t%s\t%s\n", type, desc, path, host, port); +} + +void +gcgi_print_gph(char type, char *desc, char *path, char *host, char *port) +{ + assert(type >= 0x30); + if (host == NULL) + host = "server"; + if (port == NULL) + port = "port"; + printf("[%c|%s|%s|%s|%s]\n", type, desc, path, host, port); +} + +void gcgi_template(char const *path, struct gcgi_var_list *vars) { FILE *fp; DIR diff --git a/libgcgi.h b/libgcgi.h @@ -28,8 +28,6 @@ void gcgi_fatal(char *fmt, ...); void gcgi_template(char const *path, struct gcgi_var_list *vars); /* manage a `key`-`val` pair storage `vars`, as used with gcgi_template */ -void gcgi_add_var(struct gcgi_var_list *vars, char *key, char *val); -void gcgi_sort_var_list(struct gcgi_var_list *vars); void gcgi_set_var(struct gcgi_var_list *vars, char *key, char *val); char *gcgi_get_var(struct gcgi_var_list *vars, char *key); void gcgi_free_var_list(struct gcgi_var_list *vars); @@ -41,8 +39,8 @@ int gcgi_write_var_list(struct gcgi_var_list *vars, char *path); /* components of the gopher request */ extern char *gcgi_gopher_search; extern char *gcgi_gopher_path; +extern struct gcgi_var_list gcgi_gopher_query; extern char *gcgi_gopher_host; extern char *gcgi_gopher_port; -extern struct gcgi_var_list gcgi_gopher_query; #endif