├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENCE ├── Makefile ├── README.md ├── doc ├── Makefile └── gopherd.8.xml ├── share └── mk └── src ├── Makefile ├── banner.c ├── file.c ├── gopherd.h ├── main.c ├── menu.c ├── output.c └── root.c /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ ubuntu-latest ] 14 | cc: [ clang, gcc ] 15 | make: [ bmake, pmake ] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | with: 20 | submodules: recursive 21 | 22 | - name: dependencies 23 | run: sudo apt-get install pmake bmake 24 | 25 | - name: make 26 | run: ${{ matrix.make }} -r -j 2 CC=${{ matrix.cc }} 27 | 28 | - name: install 29 | run: ${{ matrix.make }} -r -j 2 PREFIX=/tmp/p install 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "share/git/kmkf"] 2 | path = share/git/kmkf 3 | url = https://github.com/katef/kmkf 4 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | 2 | Copyright © 2007-2020 Katherine Flavel 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 17 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 18 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 20 | EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 22 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .MAKEFLAGS: -r -m share/mk 2 | 3 | # targets 4 | all:: mkdir .WAIT dep prog 5 | dep:: 6 | gen:: 7 | test:: all 8 | install:: all 9 | uninstall:: 10 | clean:: 11 | 12 | # things to override 13 | CC ?= gcc 14 | BUILD ?= build 15 | PREFIX ?= /usr/local 16 | 17 | # ${unix} is an arbitrary variable set by sys.mk 18 | .if defined(unix) 19 | .BEGIN:: 20 | @echo "We don't use sys.mk; run ${MAKE} with -r" >&2 21 | @false 22 | .endif 23 | 24 | # layout 25 | SUBDIR += src 26 | 27 | .include 28 | .include 29 | .include 30 | .include 31 | .include 32 | .include 33 | .include 34 | .include 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gopherd 2 | 3 | This is a Gopher server, run under inetd. 4 | 5 | The server is intended to be run as an inetd(8) service. 6 | It will serve one request and then exit. 7 | A suitable inetd.conf(5) configuration line looks like so: 8 | 9 | gopher stream tcp nowait nobody /path/to/gopherd gopherd [-options] 10 | 11 | ### Building from source 12 | 13 | Clone with submodules (contains required .mk files): 14 | 15 | ; git clone --recursive https://github.com/katef/gopherd.git 16 | 17 | To build and install: 18 | 19 | ; bmake -r install 20 | 21 | You can override a few things: 22 | 23 | ; CC=clang bmake -r 24 | ; PREFIX=$HOME bmake -r install 25 | 26 | You need bmake for building. In order of preference: 27 | 28 | 1. If you use some kind of BSD (NetBSD, OpenBSD, FreeBSD, ...) this is make(1). 29 | They all differ slightly. Any of them should work. 30 | 2. If you use Linux or MacOS and you have a package named bmake, use that. 31 | 3. If you use Linux and you have a package named pmake, use that. 32 | It's the same thing. 33 | Some package managers have bmake packaged under the name pmake. 34 | I don't know why they name it pmake. 35 | 4. Otherwise if you use MacOS and you only have a package named bsdmake, use that. 36 | It's Apple's own fork of bmake. 37 | It should also work but it's harder for me to test. 38 | 5. If none of these are options for you, you can build bmake from source. 39 | You don't need mk.tar.gz, just bmake.tar.gz. This will always work. 40 | https://www.crufty.net/help/sjg/bmake.html 41 | 42 | When you see "bmake" in the build instructions above, it means any of these. 43 | 44 | Building depends on: 45 | 46 | * libmagic. This is packaged for Debian as libmagic-dev. 47 | * Any BSD make. 48 | * A C compiler. Any should do, but GCC and clang are best supported. 49 | * ar, ld, and a bunch of other stuff you probably already have. 50 | 51 | To build the manpage, the following dependencies are required: 52 | 53 | * xsltproc from libxslt 54 | * docbook XSL 55 | 56 | Ideas, comments or bugs: kate@elide.org 57 | 58 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Because I always forget: groff -man -Tascii gopherd.8 2 | 3 | XSLTPROC=xsltproc 4 | XSLTFLAGS=--stringparam man.hyphenate 1 --stringparam man.justify 1 --stringparam man.hyphenate.urls 0 \ 5 | --stringparam man.break.after.slash 1 --stringparam man.hyphenate.filenames 0 \ 6 | --stringparam man.hyphenate.computer.inlines 0 --stringparam man.links.are.numbered 1 \ 7 | --stringparam man.links.list.enabled 1 8 | DOCBOOK=http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl 9 | REMOVE=rm -f 10 | INSTALL=install 11 | 12 | PREFIX?=/usr/local 13 | _MANDIR?=${PREFIX}/man 14 | 15 | all: gopherd.8 16 | 17 | clean: 18 | ${REMOVE} gopherd.8 19 | 20 | install: all 21 | ${INSTALL} -m 755 -d ${_MANDIR}/man8 22 | ${INSTALL} -m 644 gopherd.8 ${_MANDIR}/man8/ 23 | 24 | gopherd.8: gopherd.8.xml 25 | ${XSLTPROC} ${XSLTFLAGS} -o gopherd.8 ${DOCBOOK} gopherd.8.xml 26 | 27 | -------------------------------------------------------------------------------- /doc/gopherd.8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gopherd 6 | 8 7 | 8 | 9 | 10 | gopherd 11 | Internet gopher inetd server 12 | 13 | 14 | 15 | 16 | gopherd 17 | 18 | 19 | -h 20 | -? 21 | 22 | 23 | -r directory 24 | -p port 25 | -s server 26 | -u user 27 | -b banner 28 | -a 29 | -i 30 | 31 | 32 | 33 | 34 | Description 35 | 36 | gopherd serves files via the Gopher 37 | Protocol. These files are presented as a simple directory listing. 38 | Individual files from this list may be selected, and their contents 39 | served over the protocol. Special files are omitted. 40 | 41 | It is intended that a Gopher client application is used to view 42 | the listings served. 43 | 44 | The server is intended to be run as an 45 | inetd 46 | 8 47 | service. It will serve one request and 48 | then exit. For example, with an 49 | inetd.conf 50 | 5 51 | configuration line along the lines of: 52 | 53 | gopher stream tcp nowait nobody /path/to/gopherd gopherd [-options] 54 | 55 | Banner files provide a mechanism for prepending possible 56 | informational content, or as an alternative to the list of 57 | files served. 58 | 59 | The following options are supported: 60 | 61 | 62 | 63 | directory 64 | 65 | 66 | Take directory as the root 67 | path from which selectors are based. If the server is 68 | running as root then it will attempt to 69 | chroot 70 | 2 71 | to this directory before serving. 72 | If it is running as a non-root user, the directory is 73 | prepended to the client's selector and stripped from 74 | the output menu items. There is no difference in the 75 | selectors used by the client in either case. 76 | 77 | If the server is not running as root (and therefore 78 | does not attempt to 79 | inetd 80 | 8 81 | , relative paths are normalised within 82 | the given selector by way of 83 | realpath 84 | 3 85 | . Attempts to request files outside of 86 | the given are denied. 87 | 88 | 89 | 90 | 91 | port 92 | 93 | 94 | Provide as the port to which to 95 | connect to retrieve files. This should be the same port 96 | on which 97 | inetd 98 | 8 99 | is configured to serve the initial 100 | connection. If no port is given, the default for the 101 | gopher service is used. 102 | 103 | 104 | 105 | 106 | server 107 | 108 | 109 | Provide as the server to 110 | which to connect to retrieve files. This is a hostname 111 | or IP address. This should be the 112 | same server on which 113 | inetd 114 | 8 115 | is configured to serve the initial 116 | connection. If no server is given, the default is 117 | localhost. 118 | 119 | 120 | 121 | 122 | user 123 | 124 | 125 | The user to which to 126 | setuid 127 | 2 128 | . It is intended that this be an 129 | unprivileged user such as nobody. 130 | The running server's group is also switched to the 131 | given user's group. If given in combination with 132 | the user is changed after 133 | 134 | chroot 135 | 2 136 | . 137 | 138 | 139 | 140 | 141 | banner 142 | 143 | 144 | Within each directory listed, the file 145 | banner is taken as a 146 | banner file. The contents of this file are prepended 147 | as informational lines before the listing is output. 148 | The banner file is then excluded from the directory 149 | listing. 150 | 151 | The banner file format is plaintext, with lines 152 | separated by the newline character. Each line is 153 | output via the Gopher protocol as an independent 154 | informational menu item. Since clients are expected 155 | to display these lines as-is, it is advisable to 156 | keep these lines to 80 columns or below. 157 | 158 | Lines consisting of URL-style 159 | hyperlinks are read and output as appropriate 160 | menu items. The services 161 | gopher://, 162 | http:// and 163 | telnet:// are recognised. 164 | Optionally, a port specifier may be given after the 165 | server name, which defaults to the port defined for 166 | that service if unspecified. For example: 167 | 168 | http://google.com/ 169 | http://localhost:8080/someimage.png 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Disable reporting of file sizes in menus. These 178 | are enabled by default, and display sizes in 179 | human-readable form. 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | Include hidden files (that is, those who's names 188 | begin with a period '.') in the menus' generated 189 | directory listings. 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | Display a version identifier, and exit. 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | Display a brief reminder of the command-line options, 207 | and exit. 208 | 209 | 210 | 211 | 212 | 213 | 214 | Environment 215 | 216 | None. 217 | 218 | 219 | 220 | Files 221 | 222 | All files under the argument are served, 223 | save for those omitted as the server deems appropiate. 224 | 225 | The 226 | services 227 | 5 228 | database is used to look up the default port 229 | for a given service. The 230 | passwd 231 | 5 232 | database is used to find the user 233 | ID from a given username. 234 | 235 | 236 | 237 | See Also 238 | 239 | 240 | gopher 241 | 1 242 | , 243 | chroot 244 | 2 245 | , 246 | setuid 247 | 2 248 | , 249 | setgid 250 | 2 251 | , 252 | realpath 253 | 3 254 | , 255 | libmagic 256 | 3 257 | , 258 | passwd 259 | 5 260 | , 261 | services 262 | 5 263 | , 264 | inetd.conf 265 | 5 266 | , 267 | inetd 268 | 8 269 | . 270 | 271 | RFC 1436 - The Internet Gopher 272 | Protocol, RFC 1945 273 | - Hypertext Transfer Protocol, 274 | RFC 854 - Telnet Protocol 275 | Specification. 276 | 277 | 278 | 279 | Standards 280 | 281 | The protocol implemented conforms to 282 | RFC 1436. The 283 | URL links parsed in banner files 284 | are a subset of RFC 1738 - 285 | Uniform Resource Locators. 286 | 287 | 288 | 289 | Caveats 290 | 291 | 292 | 293 | There is no provision for logging events. 294 | 295 | 296 | libmagic 297 | 3 298 | is imperfect at automatically determining the type 299 | of a file from its content; sometimes menus will list inappropriate 300 | types as a result. In most cases, the server will err on the side 301 | of caution and list these as binary (item type 9). 302 | 303 | The Gopher+ protocol is not implemented. Therefore the 304 | traditional GET / 305 | work-around for file names is used for HTTP 306 | links, which are therefore HTTP version 1.0 307 | only. 308 | 309 | Gopher's search facility (menu item type 7) is not 310 | implemented. 311 | 312 | 313 | 314 | Bugs 315 | 316 | 317 | inetd 318 | 8 319 | provides no way to programmatically determine the 320 | hostname and port from which a service is started. The 321 | and options are a 322 | work-around for this. 323 | 324 | 325 | Don't use 326 | firefox 327 | 1 328 | as a Gopher client. It does not conform to the RFC. 329 | 330 | 331 | 332 | -------------------------------------------------------------------------------- /share/mk: -------------------------------------------------------------------------------- 1 | git/kmkf/share/mk -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | .include "../../share/mk/top.mk" 2 | 3 | SRC += src/banner.c 4 | SRC += src/file.c 5 | SRC += src/main.c 6 | SRC += src/menu.c 7 | SRC += src/output.c 8 | SRC += src/root.c 9 | 10 | .for src in ${SRC:M*.c} 11 | #CFLAGS.${src} += -I src 12 | .endfor 13 | 14 | CFLAGS += -D_XOPEN_SOURCE=600 15 | 16 | PROG += gopherd 17 | 18 | # XXX: would prefer to use pkgconf for libmagic, 19 | # but it isn't packaged with a .pc file (at least for Debian). 20 | LFLAGS.gopherd += -lmagic -lz -lm 21 | 22 | .for src in ${SRC:Msrc/*.c} 23 | ${BUILD}/bin/gopherd: ${BUILD}/${src:R}.o 24 | .endfor 25 | 26 | -------------------------------------------------------------------------------- /src/banner.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Banner file handling. Banners are a simple file format which is output 3 | * line-by-line as info menu types. The file itself is not included in 4 | * the menu listing. 5 | * 6 | * Within the file, URLs (on their own lines) are output as links. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "gopherd.h" 20 | 21 | char *bannerfile; 22 | 23 | /* 24 | * Tokenise a URL out to the server, port and path. If the port is not given, 25 | * it defaults as given by the protocol name. The url given is written over. 26 | * The output path may be NULL if none is given. 27 | * 28 | * Specifiying no port is permitted; the default will be used. 29 | * 30 | * Returns false on parse errors. 31 | */ 32 | static bool 33 | urlsplit(char *url, char **server, unsigned short *port, 34 | char **path, bool *defaultport, char **service) 35 | { 36 | char *p; 37 | 38 | *server = strstr(url, "://"); 39 | if (!*server) { 40 | return false; 41 | } 42 | **server = '\0'; 43 | *server += strlen("://"); 44 | *service = url; 45 | 46 | /* 47 | * Note that we find the path before the port, so that we don't confuse 48 | * any colons in the path with the port, if a port is not given. 49 | */ 50 | *path = strchr(*server, '/'); 51 | if (*path == NULL) { 52 | /* There is no path given. */ 53 | *path = ""; 54 | } else { 55 | **path = '\0'; 56 | (*path)++; 57 | } 58 | 59 | /* 60 | * Find the port, if given. 61 | */ 62 | p = strchr(*server, ':'); 63 | if (p != NULL) { 64 | *p = '\0'; 65 | p++; 66 | 67 | *port = strtol(p, NULL, 10); 68 | if (*port) { 69 | *defaultport = false; 70 | return true; 71 | } 72 | } 73 | 74 | /* 75 | * A port was not given, so we can look it up from the services database. 76 | * url has been terminated at the "://" so that it contains only a service. 77 | */ 78 | *port = getservport(url); 79 | *defaultport = true; 80 | 81 | return true; 82 | } 83 | 84 | /* 85 | * Create a menu item, only showing the port if it is not the default for that service. 86 | */ 87 | static void 88 | showurl(const enum filetype ft, const char *href, 89 | const char *server, const unsigned short port, const char *service, 90 | const char *linkpath, const bool defaultport) 91 | { 92 | if (defaultport) { 93 | menuitem(ft, href, server, port, "%s://%s/%s", service, server, linkpath); 94 | } else { 95 | menuitem(ft, href, server, port, "%s://%s:%d/%s", service, server, port, linkpath); 96 | } 97 | } 98 | 99 | /* 100 | * Decode a URL. 101 | * May output to the same string from which it reads. 102 | */ 103 | static void 104 | urldecode(const char *in, char *out) 105 | { 106 | char hex[3] = { '\0' }; 107 | 108 | while (*in != '\0') { 109 | switch(*in++) { 110 | case '%': 111 | strncpy(hex, in, 2); 112 | *out = strtol(hex, NULL, 16); 113 | in += 2; 114 | break; 115 | 116 | case '+': 117 | *out = ' '; 118 | break; 119 | 120 | default: 121 | *out = *(in - 1); 122 | break; 123 | } 124 | 125 | out++; 126 | } 127 | 128 | *out = '\0'; 129 | } 130 | 131 | /* 132 | * Will write over the memory it is passed. 133 | */ 134 | static void 135 | showbanner(char *banner) 136 | { 137 | char *p; 138 | 139 | for ( ; banner != NULL && *banner != '\0'; banner = p) { 140 | char *server; 141 | unsigned short port; 142 | char *path; 143 | char *service; 144 | bool defaultport; 145 | 146 | p = strchr(banner, '\n'); 147 | if (p != NULL) { 148 | *p++ = '\0'; 149 | } 150 | 151 | if (!strstr(banner, "://")) { 152 | listinfo(banner); 153 | continue; 154 | } 155 | 156 | /* 157 | * Here we have a url. Attempt to parse it. 158 | */ 159 | urldecode(banner, banner); 160 | if (!urlsplit(banner, &server, &port, &path, &defaultport, &service)) { 161 | listinfo(banner); 162 | continue; 163 | } 164 | 165 | if (!strncmp(service, "http", strlen("http"))) { 166 | char *s; 167 | size_t slen; 168 | 169 | slen = strlen(path) + strlen("GET /"); 170 | s = malloc(slen + 1); 171 | if (s == NULL) { 172 | listerror("malloc"); 173 | } 174 | 175 | snprintf(s, slen + 1, "GET %s%s", path[0] == '/' ? "" : "/", path); 176 | showurl(FT_HTML, s, server, port, service, path, defaultport); 177 | free(s); 178 | continue; 179 | } else if (!strncmp(service, "gopher", strlen("gopher"))) { 180 | showurl(FT_DIR, path, server, port, service, path, defaultport); 181 | continue; 182 | } else if (!strncmp(service, "telnet", strlen("telnet"))) { 183 | showurl(FT_TELNET, path, server, port, service, path, defaultport); 184 | continue; 185 | } 186 | 187 | /* 188 | * An unrecognised service. 189 | */ 190 | listinfo(banner); 191 | } 192 | 193 | listinfo(""); 194 | } 195 | 196 | /* TODO show banner here, if specified. 197 | * ^http:// and ^gopher:// etc (including telnet) can be made links. */ 198 | void 199 | listbanner(const char *path) 200 | { 201 | char *s; 202 | size_t slen; 203 | int fd; 204 | char *mm; 205 | struct stat sb; 206 | 207 | slen = strlen(bannerfile) + strlen(path) + strlen("/"); 208 | s = malloc(slen + 1); 209 | if (s == NULL) { 210 | listerror("malloc"); 211 | } 212 | snprintf(s, slen + 1, "%s/%s", path, bannerfile); 213 | 214 | errno = 0; 215 | fd = open(s, O_RDONLY); 216 | free(s); 217 | if (fd == -1) { 218 | if (errno == ENOENT) { 219 | /* A banner does not exist for this directory; this is fine. */ 220 | return; 221 | } 222 | 223 | /* A real error occured. */ 224 | listerror("open"); 225 | } 226 | 227 | if (fstat(fd, &sb) == -1) { 228 | listerror("fstat"); 229 | } 230 | 231 | /* We're mapping read/write so we can conveniently tokenise in-place with \0s */ 232 | errno = 0; 233 | mm = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 234 | if (mm == MAP_FAILED) { 235 | listerror("mmap"); 236 | } 237 | 238 | showbanner(mm); 239 | 240 | munmap(mm, sb.st_size); 241 | close(fd); 242 | } 243 | 244 | -------------------------------------------------------------------------------- /src/file.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Single file handling. 3 | */ 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #include "gopherd.h" 20 | 21 | /* 22 | * Output the given file. 23 | */ 24 | void 25 | mapfile(const char *path, size_t len) 26 | { 27 | char *mm; 28 | int fd; 29 | 30 | assert(path != NULL); 31 | 32 | if (len == 0) { 33 | return; 34 | } 35 | 36 | errno = 0; 37 | fd = open(path, O_RDONLY); 38 | if (fd == -1) { 39 | listerror("open"); 40 | } 41 | 42 | errno = 0; 43 | mm = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); 44 | if (mm == MAP_FAILED) { 45 | listerror("mmap"); 46 | } 47 | 48 | fwrite(mm, len, 1, stdout); 49 | 50 | munmap(mm, len); 51 | close(fd); 52 | } 53 | 54 | 55 | /* 56 | * Attempt to find a file's type. If the extension is not recognised, 57 | * an attempt is made to guess the file's contents via libmagic. 58 | */ 59 | enum filetype 60 | findtype(const char *ext, const char *path) 61 | { 62 | magic_t mt; 63 | const char *ms; 64 | enum filetype ft; 65 | 66 | /* TODO: replace with binary-search table. Or maybe a list of regexps */ 67 | if (ext == NULL) { 68 | goto magic; 69 | } else if (!strcasecmp(ext, "txt")) { 70 | /* plain text */ 71 | return FT_TEXT; 72 | } else if (!strcasecmp(ext, "png") 73 | || !strcasecmp(ext, "jpg") 74 | || !strcasecmp(ext, "jpeg") 75 | || !strcasecmp(ext, "bmp")) { 76 | /* image */ 77 | return FT_IMAGE; 78 | } else if (!strcasecmp(ext, "gif")) { 79 | return FT_GIF; 80 | } else if (!strcasecmp(ext, "wav") 81 | || !strcasecmp(ext, "ogg") 82 | || !strcasecmp(ext, "mp3")) { 83 | /* audio */ 84 | return FT_AUDIO; 85 | } else if (!strcasecmp(ext, "hqx") 86 | || !strcasecmp(ext, "hcx")) { 87 | /* BinHex - Also .hex, but we let libmagic determine that */ 88 | return FT_BINHEX; 89 | } 90 | 91 | magic: 92 | 93 | /* unrecognised extension: attempt to guess by contents */ 94 | mt = magic_open(MAGIC_SYMLINK | MAGIC_ERROR | MAGIC_MIME); 95 | if (!mt) { 96 | return FT_BINARY; 97 | } 98 | 99 | if (magic_load(mt, NULL) == -1) { 100 | return FT_BINARY; 101 | } 102 | 103 | ms = magic_file(mt, path); 104 | if (!ms) { 105 | return FT_BINARY; 106 | } 107 | 108 | /* TODO map in more mime types here, including UUE (6) and BinHex (4) */ 109 | if (!strncmp(ms, "text/html", strlen("text/html"))) { 110 | ft = FT_HTML; 111 | } else if (!strncmp(ms, "text/", strlen("text/"))) { 112 | ft = FT_TEXT; 113 | } else { 114 | /* still unrecognised: binary */ 115 | ft = FT_BINARY; 116 | } 117 | 118 | magic_close(mt); 119 | 120 | return ft; 121 | } 122 | 123 | -------------------------------------------------------------------------------- /src/gopherd.h: -------------------------------------------------------------------------------- 1 | #ifndef GOPHERD_H 2 | #define GOPHERD_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #define VERSION "0.1" 9 | #define AUTHOR "Kate F" 10 | 11 | /* 12 | * Gopher menu types. 13 | * These are the first character in menus. 14 | */ 15 | enum filetype { 16 | FT_BINARY = '9', 17 | FT_GIF = 'g', 18 | FT_HTML = 'h', 19 | FT_IMAGE = 'I', 20 | FT_AUDIO = 's', 21 | FT_TEXT = '0', 22 | FT_DIR = '1', 23 | FT_INFO = 'i', 24 | FT_ERROR = '3', 25 | FT_BINHEX = '4', 26 | FT_TELNET = '8' 27 | }; 28 | 29 | extern char *root; 30 | extern bool chrooted; 31 | extern bool showhidden; 32 | extern bool hidesize; 33 | extern char *bannerfile; 34 | 35 | /* main.c */ 36 | unsigned short getservport(const char *service); 37 | 38 | /* file.c */ 39 | void mapfile(const char *path, size_t len); 40 | enum filetype findtype(const char *ext, const char *path); 41 | 42 | /* menu.c */ 43 | void dirmenu(const char *path, const char *server, unsigned short port); 44 | 45 | /* root.c */ 46 | const char *striproot(const char *path); 47 | char *readandchroot(const char *user); 48 | 49 | /* output.c */ 50 | /* TODO make enums const */ 51 | void menuitem(enum filetype ft, const char *path, 52 | const char *server, const unsigned short port, 53 | const char *namefmt, ...); 54 | void listerror(const char *msg); 55 | void listinfo(const char *fmt, ...); 56 | 57 | /* banner.c */ 58 | void listbanner(const char *path); 59 | 60 | #endif 61 | 62 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * A gopher server. Menus are generated for directory listings. 3 | * This is intended to be launched from inetd. Something like: 4 | * 5 | * gopher stream tcp nowait nobody /path/to/gopherd gopherd 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "gopherd.h" 20 | 21 | static char *server = "localhost"; 22 | static unsigned short port = 70; 23 | 24 | unsigned short 25 | getservport(const char *service) 26 | { 27 | unsigned short port; 28 | struct servent *se; 29 | 30 | errno = 0; 31 | se = getservbyname(service, NULL); 32 | if (se == NULL) { 33 | listerror("getservbyname"); 34 | } 35 | 36 | port = ntohs(se->s_port); 37 | endservent(); 38 | 39 | return port; 40 | } 41 | 42 | int 43 | main(int argc, char *argv[]) 44 | { 45 | struct stat sb; 46 | char *user = NULL; 47 | char *path; 48 | 49 | port = getservport("gopher"); 50 | 51 | /* TODO some option to specify a banner file for "welcome to such-and-such server" */ 52 | /* TODO option to syslog requests */ 53 | /* TODO does inetd provide those as environment variables? */ 54 | { 55 | int c; 56 | 57 | while (c = getopt(argc, argv, "viahr:u:b:s:p:"), c != -1) { 58 | switch(c) { 59 | case 'p': 60 | port = atoi(optarg); 61 | break; 62 | 63 | case 's': server = optarg; break; 64 | case 'r': root = optarg; break; 65 | case 'u': user = optarg; break; 66 | case 'b': bannerfile = optarg; break; 67 | case 'i': hidesize = false; break; 68 | case 'a': showhidden = true; break; 69 | 70 | case 'v': 71 | /* TODO from last / for argv[0] */ 72 | printf("%s %s, %s\n", argv[0], VERSION, AUTHOR); 73 | return EXIT_SUCCESS; 74 | 75 | case 'h': 76 | case '?': 77 | default: 78 | /* TODO document usage in a manpage or somesuch */ 79 | printf("usage: %s [ -h | -a | -b | -r | -u | -s | -p ]\n", argv[0]); 80 | return EXIT_SUCCESS; 81 | } 82 | } 83 | 84 | argc -= optind; 85 | argv += optind; 86 | } 87 | 88 | path = readandchroot(user); 89 | if (path == NULL) { 90 | listerror("initialise"); 91 | } 92 | 93 | errno = 0; 94 | if (stat(path, &sb) == -1) { 95 | listerror("stat"); 96 | } 97 | 98 | if (S_ISDIR(sb.st_mode)) { 99 | dirmenu(path, server, port); 100 | printf(".\r\n"); 101 | } else { 102 | char *ext; 103 | 104 | mapfile(path, sb.st_size); 105 | 106 | /* If it's not binary, end with a '.' */ 107 | /* TODO do pictures need this? */ 108 | ext = strrchr(path, '.'); 109 | if (findtype(ext, path) != FT_BINARY) { 110 | printf(".\r\n"); 111 | } 112 | } 113 | 114 | return EXIT_SUCCESS; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/menu.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Menu listing. 3 | */ 4 | 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "gopherd.h" 18 | 19 | bool showhidden; 20 | bool hidesize; 21 | 22 | /* 23 | * Return a string of human-readable digits in the form "2.34KB". 24 | * Caller frees. 25 | */ 26 | static char * 27 | humanreadable(double size) 28 | { 29 | char buf[255]; 30 | 31 | if (size < 1) { 32 | snprintf(buf, sizeof buf, "0"); 33 | } else if (size < 1000) { 34 | snprintf(buf, sizeof buf, "%dB", (int)size); 35 | } else if (size < 1000000) { 36 | snprintf(buf, sizeof buf, "%.2fkB", size / 1024); 37 | } else { 38 | const char unit[] = "MGTP"; 39 | int i; 40 | 41 | for (i = 3; ; i++) { 42 | if (size >= pow(1000, i) && unit[i - 2]) { 43 | continue; 44 | } 45 | 46 | snprintf(buf, sizeof buf, "%.02f%cB", size / pow(1024, i - 1), unit[i - 3]); 47 | break; 48 | } 49 | } 50 | 51 | return strdup(buf); 52 | } 53 | 54 | /* 55 | * Concaternate a filename onto a path. 56 | * Caller frees. 57 | */ 58 | static char * 59 | allocpath(const char *filename, const char *path) 60 | { 61 | char *s; 62 | size_t slen; 63 | 64 | assert(filename != NULL); 65 | assert(path != NULL); 66 | 67 | slen = strlen(filename) + strlen(path) + strlen("/"); 68 | errno = 0; 69 | 70 | s = malloc(slen + 1); 71 | if (s == NULL) { 72 | listerror("malloc"); 73 | } 74 | 75 | snprintf(s, slen + 1, !strcmp(path, "/") ? "%s%s" : "%s/%s", path, filename); 76 | 77 | return s; 78 | } 79 | 80 | /* 81 | * Create a listing for a single file item. 82 | */ 83 | static void 84 | listfile(const char *filename, const char *ext, const char *parent, 85 | const char *server, const unsigned short port) 86 | { 87 | enum filetype ft; 88 | struct stat sb; 89 | char *size; 90 | char *s; 91 | 92 | assert(filename != NULL); 93 | assert(parent != NULL); 94 | 95 | ft = findtype(ext, filename); 96 | 97 | /* TODO append directory listing details: date etc */ 98 | s = allocpath(filename, parent); 99 | 100 | if (stat(s, &sb) == -1) { 101 | listerror("stat"); 102 | } 103 | 104 | if (hidesize) { 105 | menuitem(ft, striproot(s), server, port, "%s", filename); 106 | } else { 107 | size = humanreadable(sb.st_size); 108 | menuitem(ft, striproot(s), server, port, "%s - %s", filename, size); 109 | free(size); 110 | } 111 | 112 | free(s); 113 | } 114 | 115 | /* 116 | * Create a listing for a single directory item. Note that trailing 117 | * slashes are not appended, as the display is the client's choice. 118 | */ 119 | static void 120 | listdir(const char *dirname, const char *parent, 121 | const char *server, const unsigned short port) 122 | { 123 | char *s; 124 | 125 | assert(dirname != NULL); 126 | assert(parent != NULL); 127 | 128 | s = allocpath(dirname, parent); 129 | 130 | /* TODO append directory details here (number of entries) */ 131 | menuitem(FT_DIR, striproot(s), server, port, "%s", dirname); 132 | 133 | free(s); 134 | } 135 | 136 | /* 137 | * Create a menu listing all the contents of the given directory. 138 | */ 139 | void 140 | dirmenu(const char *path, const char *server, const unsigned short port) 141 | { 142 | struct dirent de; 143 | struct dirent *dep; 144 | unsigned int i; 145 | DIR *od; 146 | 147 | assert(path != NULL); 148 | 149 | errno = 0; 150 | od = opendir(path); 151 | if (od == NULL) { 152 | listerror("opendir"); 153 | } 154 | 155 | if (bannerfile != NULL) { 156 | listbanner(path); 157 | } 158 | 159 | i = 0; 160 | for (;;) { 161 | char *s; 162 | struct stat sb; 163 | 164 | errno = 0; 165 | if (readdir_r(od, &de, &dep)) { 166 | listerror("readdir_r"); 167 | } 168 | 169 | if (!dep) { 170 | break; 171 | } 172 | 173 | /* 174 | * Skip parent and current directories. 175 | */ 176 | if (!strcmp(de.d_name, ".") || !strcmp(de.d_name, "..")) { 177 | continue; 178 | } 179 | 180 | /* 181 | * Skip hidden files, if appropiate. 182 | */ 183 | if (!showhidden && de.d_name[0] == '.') { 184 | continue; 185 | } 186 | 187 | /* 188 | * Skip the banner file here, if specified, since it needn't be 189 | * included in listings. 190 | */ 191 | if (bannerfile && !strcmp(de.d_name, bannerfile)) { 192 | continue; 193 | } 194 | 195 | s = allocpath(de.d_name, path); 196 | 197 | if (stat(s, &sb) == -1) { 198 | listerror("stat"); 199 | } 200 | free(s); 201 | 202 | switch(sb.st_mode & S_IFMT) { 203 | case S_IFDIR: 204 | /* TODO possibly show a configurable level of subentries? */ 205 | listdir(de.d_name, path, server, port); 206 | i++; 207 | break; 208 | 209 | case S_IFREG: 210 | { 211 | char *ext; 212 | 213 | ext = strrchr(de.d_name, '.'); 214 | listfile(de.d_name, ext ? ext + 1 : NULL, path, server, port); 215 | } 216 | i++; 217 | break; 218 | 219 | default: 220 | /* Omit unrecognised types */ 221 | break; 222 | } 223 | } 224 | 225 | /* 226 | * If there are no items, this may be a banner-only directory. 227 | */ 228 | if (bannerfile && i) { 229 | listinfo(""); 230 | listinfo("%d item%s total", i, i == 1 ? "" : "s"); 231 | } 232 | 233 | closedir(od); 234 | } 235 | 236 | -------------------------------------------------------------------------------- /src/output.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Menu output. 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "gopherd.h" 14 | 15 | static void listtexterrorf(const char *fmt, ...); 16 | 17 | /* 18 | * Check for presence of illegal characters. Returns false if they exist. 19 | * Errors within this function are not reported to the client to avoid an 20 | * infinite loop. 21 | */ 22 | static bool 23 | checkchars(const char *s) 24 | { 25 | assert(s != NULL); 26 | 27 | /* 28 | * If the complement of this set spans to the end of the string 29 | * (that is, s[span] is '\0', then no invalid characters are 30 | * found. 31 | */ 32 | return !s[strcspn(s, "\t\r\n")]; 33 | } 34 | 35 | /* 36 | * Output a single menu item. The strings passed in are unescaped. 37 | * Errors within this function are not reported to the client to 38 | * avoid an infinite loop. 39 | */ 40 | static void 41 | vmenuitem(enum filetype ft, const char *path, 42 | const char *server, unsigned short port, 43 | const char *namefmt, va_list ap) 44 | { 45 | char s[1024]; 46 | 47 | assert(path != NULL); 48 | assert(server != NULL); 49 | assert(namefmt != NULL); 50 | 51 | if (vsnprintf(s, sizeof s, namefmt, ap) >= sizeof s) { 52 | exit(EXIT_FAILURE); 53 | } 54 | 55 | if (!checkchars(s)) { 56 | listtexterrorf("Illegal character in filename"); 57 | return; 58 | } 59 | 60 | if (!checkchars(path)) { 61 | listtexterrorf("Illegal character in path to file"); 62 | return; 63 | } 64 | 65 | if (!checkchars(server)) { 66 | listtexterrorf("Illegal character in hostname"); 67 | return; 68 | } 69 | 70 | printf("%c%s\t%s\t%s\t%d\r\n", 71 | ft, s, path, server, port); 72 | } 73 | 74 | /* 75 | * Output a single menu item. The strings passed in are unescaped. 76 | */ 77 | void 78 | menuitem(enum filetype ft, const char *path, 79 | const char *server, const unsigned short port, 80 | const char *namefmt, ...) 81 | { 82 | va_list ap; 83 | 84 | assert(path != NULL); 85 | assert(namefmt != NULL); 86 | 87 | va_start(ap, namefmt); 88 | vmenuitem(ft, path, server, port, namefmt, ap); 89 | va_end(ap); 90 | } 91 | 92 | /* 93 | * Create an error listing and exit. 94 | */ 95 | static void 96 | listtexterrorf(const char *fmt, ...) 97 | { 98 | va_list ap; 99 | 100 | va_start(ap, fmt); 101 | vmenuitem(FT_ERROR, "fake", "(NULL)", 0, fmt, ap); 102 | va_end(ap); 103 | } 104 | 105 | /* 106 | * Create an error listing and exit. 107 | */ 108 | void 109 | listerror(const char *msg) 110 | { 111 | menuitem(FT_ERROR, "fake", "(NULL)", 0, "%s: %s", msg, strerror(errno)); 112 | printf(".\r\n"); 113 | exit(EXIT_FAILURE); 114 | } 115 | 116 | /* 117 | * Create a listing for informational text. printf-style formatting is 118 | * provided. 119 | */ 120 | void 121 | listinfo(const char *fmt, ...) 122 | { 123 | va_list ap; 124 | 125 | va_start(ap, fmt); 126 | vmenuitem(FT_INFO, "fake", "(NULL)", 0, fmt, ap); 127 | va_end(ap); 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/root.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Root constraints. 3 | * This is either chrooted, or a transparently-prepended path. 4 | */ 5 | 6 | /* for chroot(2) */ 7 | #define _BSD_SOURCE 8 | #include 9 | #undef _BSD_SOURCE 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "gopherd.h" 19 | 20 | char *root; 21 | bool chrooted; 22 | 23 | /* 24 | * Strip off the prepended root path if neccessary. This is used to santize the 25 | * output when the server is unable to chroot and so has prepended the root. 26 | */ 27 | const char * 28 | striproot(const char *path) 29 | { 30 | if (root != NULL && !chrooted) { 31 | return path + strlen(root); 32 | } 33 | 34 | return path; 35 | } 36 | 37 | /* 38 | * Returns the path based from the selector read. The two operations are 39 | * combined here because the selector is prepended with the root path if a 40 | * chroot cannot be performed. Caller frees. 41 | * 42 | * The path returned always represents the path on the filesystem. It it was 43 | * prepended with the root (that is, if root is set, but chrooted is false) 44 | * then the root should be skipped before output, for consistency to the client. 45 | * This way the client is returned links which look like their origional 46 | * selector. 47 | */ 48 | char * 49 | readandchroot(const char *user) 50 | { 51 | char selector[MAXPATHLEN]; 52 | char path[MAXPATHLEN]; 53 | struct passwd *pw; 54 | 55 | /* 56 | * Find the user to switch to. This must be done before chroot, because 57 | * getpwnam() needs to read /etc/passwd. 58 | */ 59 | if (user != NULL) { 60 | errno = 0; 61 | pw = getpwnam(user); 62 | if (!pw) { 63 | listerror("unknown user"); 64 | } 65 | } 66 | 67 | /* 68 | * Perform the chroot. This must be done before changing user as chroot may only 69 | * be done by the root user. If the user is not root, we will prepend the root 70 | * path to our selector further on, as a substitute. 71 | */ 72 | chrooted = false; 73 | if (root != NULL && getuid() == 0) { 74 | if (chroot(root) == -1) { 75 | listerror("chroot"); 76 | } 77 | chrooted = true; 78 | } 79 | 80 | /* 81 | * Switch user. 82 | */ 83 | if (user != NULL && pw) { 84 | if (setgid(pw->pw_gid) == -1) { 85 | listerror("setgid"); 86 | } 87 | 88 | if (setuid(pw->pw_uid) == -1) { 89 | listerror("setuid"); 90 | } 91 | 92 | endpwent(); 93 | } 94 | 95 | /* 96 | * Find and simplify the given selector into a selection path. 97 | */ 98 | if (fgets(selector, MAXPATHLEN, stdin) == NULL) { 99 | listerror("fgets"); 100 | } 101 | selector[strcspn(selector, "\r\n")] = '\0'; 102 | if (strlen(selector) == 0) { 103 | strcpy(selector, "/"); 104 | } 105 | if (root != NULL && !chrooted) { 106 | char s[MAXPATHLEN]; 107 | 108 | strncpy(s, selector, sizeof(s) - 1); 109 | s[sizeof(s) - 1] = '\0'; 110 | snprintf(selector, MAXPATHLEN, "%s/%s", root, s); 111 | } 112 | if (!realpath(selector, path)) { 113 | listerror("realpath"); 114 | } 115 | 116 | /* 117 | * Check that the selection path is inside the given root. 118 | */ 119 | if (root != NULL && !chrooted) { 120 | if (strncmp(path, root, strlen(root))) { 121 | errno = EACCES; 122 | listerror("root"); 123 | } 124 | } 125 | 126 | return strdup(path); 127 | } 128 | 129 | --------------------------------------------------------------------------------