commit d829e6a347b28610a1f10f3a27bd8c804f5babb7 Author: Kim Holviala Date: Sat Jan 25 11:21:40 2014 +0200 Initial git commit with version 1.4 diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..a867a05 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,251 @@ +2012-12-02 Kim Holviala + + * Released version 1.4 + * Added logcheck ignore file for Debian + * Fixed compile without HAVE_SHMEM + * Autogenerated caps.txt now works without SHMEM + * Support for Haiku R1 (make && make install works) + * CGIs now have $LOCAL_ADDR + * Debian package no longer depends on lsb-release + * CPU type is now properly detected on AIX + * Makefile supports cross-compiling (CC & HOSTCC) + * Option -nr disables root user checking (for debugging) + * Platform detection for Linux/mips boards (routers) + * Fixes for OpenBSD (thanks to Brian Callahan) + * Last remaining sprintf() replaced with snprintf() + * max() is no longer a function but a #define + * Clang fixes (thanks to Jacob Dahl Pind) + * Removed system-info.sh & dmidecode from Debian package + + +2012-06-12 Kim Holviala + + * Released version 1.3 + * Support for headerless HTTP/0.9 + * Code cleanups + * Platform probing now knows about RedHat and Slackware + * lsb_release no longer gets called if it doesn't exist + (bugreport from Jacob Dahl Pind) + * Changed menu errors from type "i" text to type "3" + (bugreport from Damien Carol) + * Removed the special OSX Carbonized build + * Fixed a bug in menu.c: popen() needs pclose() + * Fixed a off-by-one in gopher_filetype() + * Changed the filetype of documents (doc/ps/pdf) to "d" + + +2012-05-04 Kim Holviala + + * Released version 1.2 (finally...) + * On the fly selector rewriting (like Apache mod_rewrite) + * Gophertags are properly converted to output charset + * Server admin email can be specified for caps.txt + * Server location for caps.txt + * Split options.c from gophernicus.c + * Removed support for Gopher over HTTP proxies + * Hack to make "gopher example.com" work + (UMN gopher client assumes gopher+ which we don't support) + + +2010-12-01 Kim Holviala + + * Released version 1.1 + * Content-based detection of gif/png/jpg/ps/pdf/html/gz + * Gophermap virtual host list (%) only lists FQDNs + * Serve out caps.txt from a file if it exists + * Option -na disables autogenerated caps.txt + * Changed option -l to -b (show BSD license) + * Changed option -m to -l (log to file) + * README and LICENSE weren't zero-terminated strings (duh) + * Querying /server-status no longer updates statistics + (because Munin statistics collection was being throttled) + * Internal charset variables are now enums and not strings + + +2010-10-05 Kim Holviala + + * Released version 1.0 + * Support for caps.txt as suggested by Cameron Kaiser + * Support for gophertags (lifted from Bucktooth) + * HTTP requests are redirected to a public gopher proxy + * Allow directories named "gophermap" and "gophertag" + (only files are special) + * Removed duplicate call to strniconv() + * A few ENABLE_STRICT_RFC1436 additions + * Much more descriptive error logging + * Self references /./ are removed from request + * Apache-compatible file logging in combined log format (-m) + * FIFOs in directories no longer crash the server (duh!) + * Support for Mac OS X (tested with 10.5 on Intel) + * Reworked Makefile with platform-specific build targets + * Fixed a call to dirname(path) (don't assume it modifies path) + * Makefile installer now supports xinetd and launchd + * Makefile installer installs default /var/gopher/gophermap and + links the document directory as /docs/ + * Added SERVER_ARCH, SERVER_VERSION and SERVER_DESCRIPTION to + CGI env variables + * Fixed a segfault where shm was used uninitialized (duh!) + * Generate native Debian/Ubuntu package with "make deb" + * platform() now tries to figure out Linux distribution + * platform() knows about Linux ARM boards + * Added argument -nm (No shared Memory) for debugging + * Fixed AIX make glitch when compiling bin2c (bug in make?) + * Changed the name of the project from "Gophernicus Server" + to plain "Gophernicus" + * Menus without footer (-nf) were missing the dot at the end + * Footer message is now right-aligned + * server-status and caps.txt requests now update sessions + * Compile-time option ENABLE_AUTOHIDING hides manually listed + resources from generated menus (to prevent double listings) + + +2010-07-03 Kim Holviala + + * Released version 0.9 + * Added option to disable HTTP-style query strings + * Fixed a regression where some binary files were served out + as text (which broke them) + * BinHex files are now mapped to filetype 4 + * Disabling vhosting disables sessions (mostly) + + +2010-04-30 Kim Holviala + + * Released version 0.8 + * Security bug in hURL handler fixed + * Error pages are now correctly generated for type 'h' + * Menu error page more compatible with clients + * New macro sstrncmp() compares without explicit sizeof() + * Removed all traces of gopher++ protocol (extra headers) + as it just didn't work with older (circa '92) servers + * Added option to disable automatic menu headers (titles) + * Do a chdir() to the resource dir before doing anything + * Double-slashes were slashed in QUERY_STRING by accident + * Relative links to external hosts work properly in gophermap + * Gophermaps can include other gophermaps with =/path/to/file + * Executable gophermaps are parsed just like static ones + + +2010-04-13 Kim Holviala + + * Released version 0.7 + * This release is feature complete, no new features in sight + * Support for NetBSD (a typo prevented building - duh) + * Replaced the poorly-working scandir() with opendir/qsort + * Directory listings (menus) are limited to 1024 entries + * Changed the filetype of movies from "v" to ";" (which sucks) + * Fixed a compatibility issue with bucktooth gophermaps + * Finally wrote decent documentation (README and INSTALL) + + +2010-04-11 Kim Holviala + + * Released version 0.6 + * platform() results are kept in shared memory + * Support for AIX 5.1 and newer + * IPv4-in-IPv6 prefix ::ffff: is removed from remote_addr + * Replaced install(1) with the install-sh script + * /server-status CPULoad can be parsed from /usr/bin/uptime + * Replaced text2c/hexdump with bin2c.c (less dependencies) + * Files with extension .q (type 7 query) are considered CGIs + * Filetype handling completely rewritten + * Configurable gopher filetypes using the "-e ext=X" argument + * Per-directory filetype overrides in gophermaps with ":ext=X" + * Refuse to serve out gophermaps (why didn't I catch this before?) + * Cleaned up main() + * Output filters - run files through an external program (php!) + + +2010-04-05 Kim Holviala + + * Released version 0.5 + * Code tested to work on 32-bit Linux/armv5tel + * Filetype 7 query errors are now handled properly + * HTTP-style query string overrides type 7 query + * Protocol detection (0/+/++) works properly + * gopher++ extra headers parsed correctly + * gopher++ works ok with a patched NSCA Mosaic! + * ISO-8859-1 (Latin-1) output + * Full UTF-8 output support (without widechars) + * Gophermaps are converted to output charset + * All type 0 output is converted to output charset + * All charset conversions can be disabled with option -no + * !Titles in gophermaps are converted to gopher menu titles + * Automatically generates gopher title resources for menus + * Compile-time option to strictly adhere to RFC 1436 + * Compile-time option to disable all gopher++ support + + +2010-03-29 Kim Holviala + + * Released version 0.4 + * Renamed the project to "Gophernicus Server" + * Major rewrite with much cleaner code + * Changed all strncpy's to the OpenBSD strlcpy + * Server can guess the request type (menu/text/binary) + * Errors are formatted for current filetype (menu/text) + * Errors for images (types g&I) are outputted as an image + * /~luser (/home/luser/public_gopher) must be owned by luser + * Configurable output width for menus + * Files are outputted using sendfile() if available + * Locale forced to POSIX for strftime() + * Filetype '-' in gophermaps hides files + * Gopher+ requests are now handled gracefully + * Relative selectors in gophemaps work + * Filesizes in menus are now human-readable (KB/MB/GB etc) + * Refuse to serve world-writeable content + * Support for Apache-style /server-status + * HTTP requests for /server-status work (munin monitoring ftw!) + * Session tracking using shared memory + * Referer support for CGIs + * Replaced iconv() with own charset conversion routine + * Automatic throttling for users who hit the server too much + * Replaced static compile-time uname with uname() + * Support for virtual hosting (with gopher0 no less!) + + +2010-01-07 Kim Holviala + + * Released version 0.3 + * Automatic detection of text vs. binary filetype for files + which have no (known) suffix + * hURL redirect pages now respect -f (no footer) option + * IPv6 support for logging & CGI REMOTE_ADDR + * Inline gophermaps + * Removed support for relative resource names in gophermaps + * Support for virtual userdirs (~user -> /home/user/public_gopher) + * Automatic listing of userdirs in gophermap + * Redirect accidental http requests to gopher + * License included in the binary -> install no longer installs docs + * Changed command line options (I was running out of arg letters...) + * Debug to syslog with '-d' option + + +2010-01-02 Kim Holviala + + * Released version 0.2 + * Logging to syslog + * Support for gophermaps + * Support for executable gophermaps + * Support for CGI scripts + * Support for type 7 search queries + * Support for hURL redirect pages + * Support for non-ASCII resource/file names (tested with UTF-8) + * Support for both %hex and #octal request encodings + * Try to get server hostname from $HOSTNAME or gethostname() + * Basic support for different platforms via HAVE_XX defines + + +2009-12-30 Kim Holviala + + * Released version 0.1 + * Basic rfc1436 functionality works + * Fancy menus with file dates & sizes + * Options via command line (with sensible defaults) + + +2009-12-28 Kim Holviala + + * Started coding kgopherd + * Trying to remember how "C" works... + diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..f1a0ddd --- /dev/null +++ b/INSTALL @@ -0,0 +1,113 @@ +Compiling and installing Gophernicus +==================================== + +Gophernicus requires a C compiler but no extra libraries aside +from standard LIBC ones. Care has been taken to use only +standard POSIX syscalls so that it should work pretty much on +any *nix system. Compiling has only been tested with GCC, but +it will most likely work with others too. + +To compile and install run: + +$ gzip -cd gophernicus-*.tar.gz | tar xvf - +$ cd gophernicus-* +$ make +$ sudo make install + +Then add the below line to your /etc/inetd.conf and restart +inetd. If your system comes with something else than standard +inetd "make install" should have done the right thing already. + +gopher stream tcp nowait nobody /usr/sbin/in.gophernicus in.gophernicus -h + +The -h parameter is mandatory for a properly working +Gophernicus. Other parameters can also be added, see the full +list by running "/usr/sbin/in.gophernicus -?" + +By default Gophernicus serves gopher documents from /var/gopher +although that can be changed by using the -r parameter. +To enable virtual hosting create hostname directories under +the gopher root and make sure you have at least the primary +hostname (the one set with -h ) directory available +(mkdir /var/gopher/$HOSTNAME). + + +Compiling on Debian Linux (and Ubuntu) +====================================== + +The above commands work on Debian just fine, but if you prefer +having everything installed as packages run "make deb" instead +of plain "make". If all the dependencies were in place you'll +end up with an offical-looking deb package in the parent +directory (don't ask - that's just how it works). And instead +of "sudo make install" you should just install the deb with +"dpkg -i ../gophernicus_*.deb" after which It Should Just +Work(tm). + + +Compiling on Mac OS X +===================== + +When you run "make install" on OSX-like system with launchd +the install routine also installs a plist file and tells launchd +to start the server up. In other words, It Just Works(tm). + + +Cross-compiling +=============== + +Cross-compiling to a different target architecture can be done +by defining HOSTCC and CC to be different compilers. HOSTCC +must point to a local arch compiler, and CC to the target +arch one. + +$ make HOSTCC=gcc CC=target-arch-gcc + + +Shared memory issues +==================== + +Gophernicus uses SYSV shared memory for session tracking and +statistics. It creates the shared memory block using mode 600 +and a predefined key which means that a shared memory block +created with one user cannot be used by another user. Simply +said, running in.gophernicus as yourself will allocate that +memory, and then running the binary through inetd as another +user (nobody) will be denied access to that memory. + +If that happens you can simply delete the memory block and +let Gophernicus recreate it - no harm done. + +$ su - +# ipcs -m | grep beeb +# ipcrm -M + + +Porting to different platforms +============================== + +If you need to port Gophernicus to a new platform, please take +a look at gophernicus.h which has a bunch of HAVE_* #defines. +Fiddling with those usually makes it possible to compile a working +server. If you succeed in compiling Gophernicus to a new +platform please send the patches to kim@holviala.com so I can +include them into the next release. + +Tested (and semi-supported) platforms include: + + OS Arch Compiler + +-------------+-------------+-------------+ + AIX 5.1 POWER3 gcc 4 + AIX 6.1 POWER4 gcc 4 + AIX 7.1 POWER5 gcc 4 + CentOS 5.5 x86_64 gcc 4 + Debian Linux x86_64 gcc 4 + Debian Linux armv5tel gcc 4 + MacOSX 10.5 i386 gcc 4 + MacOSX 10.7 x86_64 llvm-gcc 4 + MacOSX 10.8 x86_64 clang 3 + NetBSD 5.0 x86_64 gcc 4 + Haiku R1 i386 gcc 2 + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a6b0c1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Gophernicus - Copyright (c) 2009-2012 Kim Holviala +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1d46319 --- /dev/null +++ b/Makefile @@ -0,0 +1,248 @@ +## +## Gophernicus server Makefile +## + +# +# Variables and default configuration +# +NAME = gophernicus +PACKAGE = $(NAME) +BINARY = in.$(NAME) +VERSION = 1.4 + +SOURCES = $(NAME).c file.c menu.c string.c platform.c session.c options.c +HEADERS = functions.h files.h +OBJECTS = $(SOURCES:.c=.o) +DOCS = LICENSE README INSTALL TODO ChangeLog README.Gophermap gophertag + +INSTALL = PATH=$$PATH:/usr/sbin ./install-sh -o 0 -g 0 +DESTDIR = /usr +SBINDIR = $(DESTDIR)/sbin +DOCDIR = $(DESTDIR)/share/doc/$(PACKAGE) + +ROOT = /var/gopher +OSXROOT = /Library/GopherServer +WRTROOT = /gopher +MAP = gophermap + +INETD = /etc/inetd.conf +XINETD = /etc/xinetd.d +LAUNCHD = /Library/LaunchDaemons +PLIST = org.gophernicus.server.plist +NET_SRV = /boot/common/settings/network/services + +DIST = $(PACKAGE)-$(VERSION) +TGZ = $(DIST).tar.gz +RELDIR = /var/gopher/gophernicus.org/software/gophernicus/server/ + +CC = gcc +HOSTCC = $(CC) +CFLAGS = -O2 -Wall +LDFLAGS = + + +# +# Platform support, compatible with both BSD and GNU make +# +all: + @case `uname` in \ + Darwin) $(MAKE) ROOT="$(OSXROOT)" $(BINARY); ;; \ + Haiku) $(MAKE) EXTRA_LDFLAGS="-lnetwork" $(BINARY); ;; \ + *) $(MAKE) $(BINARY); ;; \ + esac + +generic: $(BINARY) + + +# +# Special targets +# +deb: + dpkg-buildpackage -rfakeroot -uc -us + + +# +# Building +# +$(NAME).c: $(NAME).h $(HEADERS) + +$(BINARY): $(OBJECTS) + $(CC) $(LDFLAGS) $(EXTRA_LDFLAGS) $(OBJECTS) -o $@ + +.c.o: + $(CC) -c $(CFLAGS) $(EXTRA_CFLAGS) -DVERSION="\"$(VERSION)\"" -DDEFAULT_ROOT="\"$(ROOT)\"" $< -o $@ + + +headers: $(HEADERS) + @echo + +functions.h: + echo "/* Automatically generated function definitions */" > $@ + echo >> $@ + grep -h "^[a-z]" $(SOURCES) | grep -v "int main" | sed -e "s/ =.*$$//" -e "s/ *$$/;/" >> $@ + @echo + +bin2c: bin2c.c + $(HOSTCC) bin2c.c -o $@ + @echo + +files.h: bin2c + sed -n -e "1,/^ $$/p" README > README.options + ./bin2c -0 -n README README.options > $@ + ./bin2c -0 LICENSE >> $@ + ./bin2c -n ERROR_GIF error.gif >> $@ + @echo + + +# +# Cleanup after building +# +clean: clean-build clean-deb + +clean-build: + rm -f $(BINARY) $(OBJECTS) $(TGZ) $(HEADERS) README.options bin2c + +clean-deb: + if [ -d debian/$(PACKAGE) ]; then fakeroot debian/rules clean; fi + + +# +# Install targets +# +install: + @case `uname` in \ + Darwin) $(MAKE) ROOT="$(OSXROOT)" install-files install-docs install-root install-osx install-done; ;; \ + Haiku) $(MAKE) SBINDIR=/boot/common/bin DOCDIR=/boot/common/share/doc/$(PACKAGE) \ + install-files install-docs install-root install-haiku install-done; ;; \ + *) $(MAKE) install-files install-docs install-root; ;; \ + esac + @if [ -d "$(XINETD)" ]; then $(MAKE) install-xinetd install-done; fi + @if [ -f "$(INETD)" ]; then $(MAKE) install-inetd; fi + +.PHONY: install + +install-done: + @echo + @echo "======================================================================" + @echo + @echo "Gophernicus has now been succesfully installed. To try it out, launch" + @echo "your favorite gopher browser and navigate to this URL:" + @echo + @echo " gopher://`hostname`/" + @echo + @echo "======================================================================" + @echo + +install-files: + mkdir -p $(SBINDIR) + $(INSTALL) -s -m 755 $(BINARY) $(SBINDIR) + @echo + +install-docs: + mkdir -p $(DOCDIR) + $(INSTALL) -m 644 $(DOCS) $(DOCDIR) + @echo + +install-root: + if [ ! -d "$(ROOT)" ]; then \ + mkdir -p $(ROOT); \ + $(INSTALL) -m 644 $(MAP) $(ROOT); \ + ln -s $(DOCDIR) $(ROOT)/docs; \ + fi + @echo + +install-inetd: + @echo + @echo "======================================================================" + @echo + @echo "Looks like your system has the traditional internet superserver inetd." + @echo "Automatic installations aren't supported, so please add the following" + @echo "line to the end of your /etc/inetd.conf and restart or kill -HUP the" + @echo "inetd process." + @echo + @echo "gopher stream tcp nowait nobody $(SBINDIR)/$(BINARY) $(BINARY) -h `hostname`" + @echo + @echo "======================================================================" + @echo + +install-xinetd: + if [ -d "$(XINETD)" -a ! -f "$(XINETD)/$(NAME)" ]; then \ + sed -e "s/@HOSTNAME@/`hostname`/g" $(NAME).xinetd > $(XINETD)/$(NAME); \ + [ -x /sbin/service ] && /sbin/service xinetd reload; \ + fi + @echo + +install-osx: + if [ -d "$(LAUNCHD)" -a ! -f "$(LAUNCHD)/$(PLIST)" ]; then \ + sed -e "s/@HOSTNAME@/`hostname`/g" org.gophernicus.server.plist > \ + $(LAUNCHD)/$(PLIST); \ + launchctl load $(LAUNCHD)/$(PLIST); \ + fi + @echo + chown -h root:admin $(ROOT) $(ROOT)/* + chmod -h 0775 $(ROOT) $(ROOT)/docs + @echo + +install-haiku: + if [ -f "$(NET_SRV)" -a ! "`grep -m1 gopher $(NET_SRV)`" ]; then \ + (echo ""; \ + echo "service gopher {"; \ + echo " family inet"; \ + echo " protocol tcp"; \ + echo " port 70"; \ + echo " launch in.gophernicus -h `hostname`"; \ + echo "}") >> $(NET_SRV); \ + fi + @echo + chown user:root $(DOCDIR)/* $(SBINDIR)/$(BINARY) $(ROOT)/$(MAP) + @echo + ps | grep net_server | grep -v grep | awk '{ print $$2 }' | xargs kill + nohup /boot/system/servers/net_server >/dev/null 2>/dev/null & + @echo + +# +# Uninstall targets +# +uninstall: uninstall-xinetd uninstall-launchd + rm -f $(SBINDIR)/$(BINARY) + for DOC in $(DOCS); do rm -f $(DOCDIR)/$$DOC; done + rmdir -p $(SBINDIR) $(DOCDIR) 2>/dev/null || true + @echo + +uninstall-xinetd: + if grep -q $(BINARY) "$(XINETD)/gopher" 2>/dev/null; then \ + rm -f $(XINETD)/gopher; \ + [ -x /sbin/service ] && service xinetd reload; \ + fi + @echo + +uninstall-launchd: + if [ -f $(LAUNCHD)/$(PLIST) ]; then \ + launchctl unload $(LAUNCHD)/$(PLIST); \ + rm -f $(LAUNCHD)/$(PLIST); \ + fi + if [ -L $(ROOT) ]; then \ + rm -f $(ROOT); \ + fi + @echo + + +# +# Release targets +# +dist: clean functions.h + mkdir -p /tmp/$(DIST) + tar -cf - ./ | (cd /tmp/$(DIST) && tar -xf -) + (cd /tmp/ && tar -cvf - $(DIST)) | gzip > $(TGZ) + rm -rf /tmp/$(DIST) + +release: dist + cp $(TGZ) $(RELDIR) + + +# +# List all C defines +# +defines: + $(CC) -dM -E $(NAME).c + diff --git a/README b/README new file mode 100644 index 0000000..961c52e --- /dev/null +++ b/README @@ -0,0 +1,237 @@ +Gophernicus - Copyright (c) 2009-2012 Kim Holviala + +Gophernicus is a modern full-featured (and hopefully) secure gopher +daemon for inetd. It is licensed under the BSD license. + +Command line options: + -h hostname Change server hostname (FQDN) [$HOSTNAME] + -p port Change server port [70] + -r root Change gopher root [/var/gopher] + -t type Change default gopher filetype [0] + -g mapfile Change gophermap file [gophermap] + -a tagfile Change gophertag file [gophertag] + -c cgidir Change CGI script directory [/cgi-bin/] + -u userdir Change users personal gopherspace [public_gopher] + -l logfile Log to Apache-compatible combined format logfile + + -w width Change default page width [70] + -o charset Change default output charset [US-ASCII] + + -s seconds Session timeout in seconds [1800] + -i hits Maximum hits until throttling [4096] + -k kbytes Maximum transfer until throttling [4194304] + + -f filterdir Specify directory for output filters + -e ext=type Map file extension to gopher filetype + -R old=new Rewrite the beginning of a selector + + -D text|file Set or load server description for caps.txt + -L text|file Set or load server location for caps.txt + -A admin Set admin email for caps.txt + + -nv Disable virtual hosting + -nl Disable parent directory links + -nh Disable menu header (title) + -nf Disable menu footer + -nd Disable dates and filesizes in menus + -nc Disable file content detection + -no Disable charset conversion for output + -nq Disable HTTP-style query strings (?query) + -ns Disable logging to syslog + -na Disable autogenerated caps.txt + -nm Disable shared memory use (for debugging) + -nr Disable root user checking (for debugging) + + -d Debug to syslog (not for production use) + -b Display the BSD license + -? Display this help + + +Setting up a gopher site +======================== + +After succesfully installing Gophernicus (see INSTALL) you need to set +up the gopher root directory. By default Gophernicus serves documents +from /var/gopher so start by creating that directory and making sure +it's world-readable. Then, simply add files and directories under your +root, fire up a gopher browser (Firefox, Lynx) and open up this URL: + +gopher:/// (where is your servers hostname) + +That's it, your first gopher site is now up and running. If the links +on the root menu don't work, make sure you are using the -h +parameter in your inetd.conf (with a valid resolveable hostname +instead of - see INSTALL). + + +Security +======== + +Gophernicus has been written with high security in mind. There should +be no buffer overflows or memory allocation problems so it should be +safe to run a publicly available gopher server with Gophernicus. + +However, the security settings (which are non-changeable) are so strict +that you need to keep one thing in mind. Gophernicus will only serve +world-readable content. Being readable by the server process is not +enough, all files and directories MUST be world-readable or they are +simply hidden from all listings and denied if a client asks for them. + + +Gophermaps +========== + +By default all gopher menus are automatically generated from the +content of the directory being viewed. If you want to have +informational text along with the files, or if you want to completely +replace the generated menu with your own you need to take a look at +gophermaps. See the file README.gophermap for more information. + + +Gophertags +========== + +A gophertag file can be used to virtually rename a directory. Let's +assume that you have a directory called "foo" somewhere - it will +be listed as "foo" in all automatically generated menus. Now if you +create a file foo/gophertag and put the text "bar" into it the menus +will show "bar" but the links will still point to "foo". This is +useful for creating descriptive names for directories without +littering the file system with spaces and weird characters. + + +Personal gopherspaces +===================== + +Gophernicus supports users personal gopherspaces. If a user has +world-readable directory called public_gopher/ under his home, a +request for gopher:///1/~user/ will serve documents from +that directory. + + +Virtual hosting +=============== + +Gophernicus supports virtual hosting, or serving more than one logical +domain using the same IP address. Since gopher (RFC1436) doesn't +support virtual hosting this requires some clever (but mostly invisble) +hacks. + +To enable virtual hosting create one or more directories under your +gopher root which are named after your domain names. The primary vhost +directory (set with the -h option) must exist or virtual +hosting will be disabled. Then simply add content to the hostname +directories and you're up and running. + +Almost. + +To make gopher clients work properly with virtual hosting, create a +root gophermap for each of your domains and include the "%" type +character to create a list of all available virtual hosts (see +README.gophermap). The generated virtual host links will be created so +that standard gopher clients will find the correct domain even when +they don't specifically tell the server which host they're trying to +reach. + + +CGI support +=========== + +Gophernicus supports most parts of the CGI/1.1 standard. Most standard +CGI variables are set, and some non-standard ones are added. + +By default all scripts and binaries under any directory called +/cgi-bin/ are executed as CGI scripts (this includes cgi-bin +directories under users personal gopherspaces). Also, if a gophermap +is marked executable it is also processed as an CGI script. + +As with regular files, CGI scripts must be world-executable (and +readable) or they will be ignored. Make sure your CGI script is safe +with ANY user input as poorly coded CGI scripts are the number #1 +security problem with publicly open *nix servers. + + +Output filtering and PHP support +================================ + +In addition to CGI scripts Gophernicus supports output filtering +scripts. By default output filtering is turned off, but you can turn +it on by using the -f option, creating that directory +and creating one or more scripts in there named by either the file +suffix, or by the gopher filetype char. + +If a file is to be served out which matches either the file suffix +script, or the filetype script then instead of simply sending the +file to client the output filter script is executed with the +original file as the first parameter and the output of the script +is then sent to client. + +For PHP support install the CLI version of the PHP interpreter and +then symlink (or copy) that binary to the directory specified with +-f option using the destination name "php". + +$ ln -s /usr/bin/php5-cli /usr/lib/gophernicus/filters/php + +After that all files with the php suffix will be "filtered" through +the PHP command line interpreter. In other words, PHP starts working. +And don't use the CGI version of PHP as it outputs HTTP headers the +gopher protocol doesn't have. + + +Charset support and conversions +=============================== + +Gophernicus supports three charsets: US-ASCII, ISO-8859-1 and UTF-8. +All textual input is internally upconverted to UTF-8 and then +downconverted to whatever charset the client is asking for. The +conversion is input autosensing which means that you don't have to +specify your filesystem charset, or the charset of your text files - +it's all detected automatically. + +With standard gopher clients this is a bit of a problem as your text +files WILL be converted to 7-bit US-ASCII. This means that all 8-bit +charaters WILL BE LOST. This decision was made because no gopher +client that I tested was reliably cabable of decoding anything else +than pure US-ASCII. If you want to disable the conversion use the +"-no" option, or if you'd like to change the default output charset to +something else than US-ASCII just use for example the "-o ISO-8859-1" +option. + + +Selector rewriting +================== + +Selector rewriting lets you rewrite parts of the selector on the fly. +Well, not parts, but really just the start of it. And the rewrite +enging here is nothing like Apache's mod_rewrite as I was too lazy +to integrate any regex libraries... So, all it does is rewrite a +fixed string at the start of the selector to something else. This +will let you move your directories around while making sure that +existing deeplinks still work. + +Examples: + + -R "/~user=/~luser" + -R "/old-dir=/new-dir" + + +Session tracking and statistics +=============================== + +To enable virtual hosting with gopher (RFC1436) clients Gophernicus +tracks users and their session. As a side effect of that session +tracking, Gophernicus has simple throttling controls to keep nasty +users from killing your precious 120MHz PPC 604e server from dying +under the load. The throttling defaults are high enough that normal +human users will never hit the limits, but it's possible (and mostly +preferrable) that a badly behaving crawling agent will be throttled. + +The current sessions and other real-time status data can be viewed +by opening the URL gopher:///0/server-status . This status +view has been modeled after the Apache server-status which means +that it's possible to integrate Gophernicus into existing server +monitoring systems. To ease up such integrations, Gophernicus +supports HTTP requests of the server-status page using an URL like +http://:70/server-status?auto + + diff --git a/README.Gophermap b/README.Gophermap new file mode 100644 index 0000000..cf6ff06 --- /dev/null +++ b/README.Gophermap @@ -0,0 +1,109 @@ +!Sample gophermap for Gophernicus + +## +## This is a sample gophermap. +## +# +Creating a file called "gophermap" into a directory disables the +normal resource listing and replaces it with the contents of the map +file. You can also have inline gophermaps - files with a ".gophermap" +extension are parsed as gophermaps and displayed in between normal +resources in alphabetical order. + +In a gophermap any line that doesn't contain a character is +automatically converted to an type "i" gopher resource which are +displayed as plain text in the client. Lines which contain tabs are +intepreted as gopher resource lines which the client will render as +links. The first line of a gophermap should be a !Title line +describing the menu. + +Dynamic gophermaps are possible by making the gophermap a script and +marking it as executable. All script output is parsed just like a +static gophermap, for example lines without tabs are converted to "i" +resources. Executable gophermaps are always ran through the default +shell (/bin/sh) so depending on your operating system that's either +slow, or really unbearably slow... + +The format of a gophermap resource line is simple: +Xnameselectorhostport + +Where: + X is the gopher filetype + name is an explanation of the resource + selector is the path to resource + host:port are the hostname and port number to go to + +Type and name are mandatory. If you don't specify a selector, the +name field will be also used as the selector. If you don't specify +host or port the host:port of the current server are used instead. +Also make sure to use ONLY ONE TAB between the fields. + +Valid filetypes include: + 0 text file + 1 directory + 3 error message + 5 archive file (zip, tar etc) + 7 search query + 8 telnet session + 9 binary file + g GIF image + h HTML file + i info text + I generic image file (other than GIF) + d document file (ps, pdf, doc etc) + s sound file + ; video file + c calendar file + M MIME file (mbox, emails etc) + +Additional type characters supported by Gophernicus: + # comment - rest of the line is ignored + !title menu title (use on the first line) + -file hide the file from listings + :ext=type change filetype (for this directory only) + ~ include a list of users with valid ~/public_gopher + % include a list of available virtual hosts + =mapfile include or execute other gophermap + * stop processing gophermap, include file listing + . stop processing gophermap (default) + +Examples of valid resource lines: + +1subdir +1Relative internal link subdir +1Absolute internal link /subdir +1External link / gopher.floodgap.com 70 +1External relative link (which shouldn't work) subdir/ gopher.domain.dom 70 +0Finger-to-gopher link kim holviala.com 79 +hLink to a website URL:http://www.google.com/ + +hLink to a local html page /path/to/file.html +5Link to an tar archive /path/to/archive.tar.gz +9Link to a binary file /path/to/binary + +7Search engine query /query +8Telnet session kim holviala.com 79 + +# Hide a few files from the menu listing generated by * +-hiddenfile.txt +-hiddendir + +# Change filetypes for this directory +:png=g +:foo=b + +Include links to users own gopherspaces: +~ + +List all available virtual hosts: +% + +Include sub-gophermap: +=LICENSE + +Execute script and parse output as subgophermap: +=/usr/bin/uptime + +Here we stop processing the gophermap and include the regular menu: +* + diff --git a/TODO b/TODO new file mode 100644 index 0000000..6cf7cb2 --- /dev/null +++ b/TODO @@ -0,0 +1,10 @@ +Things I might work on: + + * Fix vhost part of README + * Support for config files + * Standalone pre-forked (threaded?) version + * Password-protected resources + * Allow/Deny per IP/host + * Automated feature testing script + * SSL/TLS + diff --git a/bin2c.c b/bin2c.c new file mode 100755 index 0000000..e6789e0 --- /dev/null +++ b/bin2c.c @@ -0,0 +1,67 @@ +/* + * Convert any file into a C #define + * + * Yes, this would have been a perl one-liner, but I didn't want + * to include compile-time dependency for perl... + */ + +#include +#include + +int main(int argc, char *argv[]) +{ + FILE *fp; + char *source = NULL; + char *name = NULL; + int first = 1; + int zero = 0; + int c; + int i; + + /* Parse args */ + while ((c = getopt(argc, argv, "n:0")) != -1) { + switch(c) { + case 'n': name = optarg; break; + case '0': zero = 1; break; + } + } + + source = argv[optind]; + if (!name) name = source; + + /* Check args */ + if (!source) { + fprintf(stderr, "Usage: %s [-0] [-n ] \n", argv[0]); + return 1; + } + + /* Try to open the source file */ + if ((fp = fopen(source, "r")) == NULL) { + perror("Couldn't open source file"); + return 1; + } + + /* Convert */ + printf("/* Automatically generated from %s */\n\n" + "#define %s { \\\n", source, name); + + do { + for (i = 0; i < 16; i++) { + if ((c = fgetc(fp)) == EOF) { + if (zero--) c = '\0'; + else break; + } + + if (i == 0 && !first) printf(", \\\n"); + if (i > 0) printf(", "); + + printf("0x%02x", c); + first = 0; + } + } while (c != EOF); + + printf("}\n\n"); + fclose(fp); + return 0; +} + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..c97e0e3 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,37 @@ +gophernicus (1.4) unstable; urgency=low + + * New upstream release + + -- Kim Holviala Sun, 02 Dec 2012 09:54:55 +0200 + +gophernicus (1.3) unstable; urgency=low + + * New upstream release + * Fixed one off-by-one + + -- Kim Holviala Tue, 12 Jun 2012 13:07:46 +0300 + +gophernicus (1.2) unstable; urgency=low + + * New upstream release + + -- Kim Holviala Fri, 04 May 2012 22:05:01 +0300 + +gophernicus (1.1) unstable; urgency=low + + * New upstream release + * Security update, so upgrading is recommended + + -- Kim Holviala Sat, 16 Oct 2010 08:54:13 +0300 + +gophernicus (1.0) unstable; urgency=low + + * Initial debianized release + * File logging by default, installs logrotate config + * Pre-configures php support but only suggests php5-cli + * Create a default gopher root + * Installs an @reboot cron entry to snoop system type + using a dmidecode script (which must be run as root) + + -- Kim Holviala Mon, 27 Sep 2010 08:11:12 +0300 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7ed6ff8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..801f365 --- /dev/null +++ b/debian/control @@ -0,0 +1,16 @@ +Source: gophernicus +Section: net +Priority: extra +Maintainer: Kim Holviala +Build-Depends: debhelper (>= 5) +Standards-Version: 3.7.3 +Homepage: gopher://gophernicus.org/1/software/gophernicus/server/ + +Package: gophernicus +Architecture: any +Depends: ${shlibs:Depends}, debconf, netbase, openbsd-inetd | inet-superserver +Recommends: lsb-release +Suggests: lynx, php5-cli +Description: Modern full-featured gopher server for inetd + Gophernicus is a modern full-featured (and hopefully) secure + gopher daemon for inetd. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..562a65a --- /dev/null +++ b/debian/copyright @@ -0,0 +1,22 @@ +Gophernicus - Copyright (c) 2009-2010 Kim Holviala +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/debian/dirs b/debian/dirs new file mode 100644 index 0000000..cf22492 --- /dev/null +++ b/debian/dirs @@ -0,0 +1,4 @@ +usr/sbin +var/gopher +var/log/gophernicus +usr/lib/gophernicus/filters diff --git a/debian/docs b/debian/docs new file mode 100644 index 0000000..3e6903c --- /dev/null +++ b/debian/docs @@ -0,0 +1,5 @@ +README +README.Gophermap +TODO +INSTALL +gophertag diff --git a/debian/gophernicus.config b/debian/gophernicus.config new file mode 100755 index 0000000..9818e75 --- /dev/null +++ b/debian/gophernicus.config @@ -0,0 +1,14 @@ +#!/bin/sh + +# Source debconf library +. /usr/share/debconf/confmodule + +# Ask for a hostname +db_input high gophernicus/fqdn || true +db_go + +# Clear old config on reconfigure +if [ "$1" = "reconfigure" ]; then + update-inetd --remove "## gopher" +fi + diff --git a/debian/gophernicus.logcheck.ignore.server b/debian/gophernicus.logcheck.ignore.server new file mode 100644 index 0000000..7ae73ea --- /dev/null +++ b/debian/gophernicus.logcheck.ignore.server @@ -0,0 +1,3 @@ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ in.gophernicus\[[0-9]+\]: request for "[^"]*" from [._[:alnum:]:-]+$ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ in.gophernicus\[[0-9]+\]: error "No such file or directory" for request "[^"]*" from [._[:alnum:]:-]+$ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ in.gophernicus\[[0-9]+\]: error "User not found" for request "/~[^"]*" from [._[:alnum:]:-]+$ diff --git a/debian/gophernicus.logrotate b/debian/gophernicus.logrotate new file mode 100644 index 0000000..d8bde2a --- /dev/null +++ b/debian/gophernicus.logrotate @@ -0,0 +1,7 @@ +/var/log/gophernicus/server.log { + rotate 6 + weekly + compress + missingok + notifempty +} diff --git a/debian/gophernicus.templates b/debian/gophernicus.templates new file mode 100644 index 0000000..a68f93a --- /dev/null +++ b/debian/gophernicus.templates @@ -0,0 +1,4 @@ +Template: gophernicus/fqdn +Type: string +Default: +Description: Fully-qualified hostname for the gopher server: diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..8d73fe1 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,37 @@ +#!/bin/sh + +#DEBHELPER# + +# Source debconf library. +. /usr/share/debconf/confmodule + +# Configuration +HOSTNAME="`hostname`" +ROOT=/var/gopher +PACKAGE=gophernicus +LOGDIR=/var/log/$PACKAGE +LIBDIR=/usr/lib/$PACKAGE +BINARY=in.gophernicus +USER=nobody + +# Get hostname from debconf +db_get gophernicus/fqdn +if [ "$RET" ]; then + HOSTNAME="`echo $RET | tr -cd 'A-Za-z0-9.-'`" +fi + +# Generate inetd service +SERVICE="gopher\t\tstream\ttcp\tnowait\t$USER\t/usr/sbin/$BINARY\t$BINARY -h $HOSTNAME -l $LOGDIR/server.log -f /usr/lib/$PACKAGE/filters" + +# Configure gophernicus +if [ "$1" = "configure" ]; then + if [ ! -f "$ROOT/gophermap" ]; then + cp $LIBDIR/gophermap $ROOT/ + fi + + chown $USER.adm $LOGDIR + chmod 0750 $LOGDIR + + update-inetd --add "$SERVICE" +fi + diff --git a/debian/postrm b/debian/postrm new file mode 100644 index 0000000..96e50f2 --- /dev/null +++ b/debian/postrm @@ -0,0 +1,8 @@ +#!/bin/sh + +if [ "$1" = "purge" ]; then + update-inetd --remove "## gopher" +fi + +#DEBHELPER# + diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 0000000..2a3251c --- /dev/null +++ b/debian/prerm @@ -0,0 +1,6 @@ +#!/bin/sh + +update-inetd --disable gopher + +#DEBHELPER# + diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..1bce626 --- /dev/null +++ b/debian/rules @@ -0,0 +1,85 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Gophernicus debian/rules that uses debhelper. +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +# Installation directory +DEST=$(CURDIR)/debian/gophernicus + +configure: + +build: build-stamp + +build-stamp: + dh_testdir + + $(MAKE) + + touch $@ + +clean: + dh_testdir + dh_testroot + rm -f build-stamp + + $(MAKE) clean-build + + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + + # Add here commands to install the package into debian/gophernicus + $(MAKE) DESTDIR=$(DEST)/usr install-files + rm -rf $(DEST)/tmp + ln -s /usr/share/doc/gophernicus/ $(DEST)/var/gopher/docs + ln -s /usr/bin/php5 $(DEST)/usr/lib/gophernicus/filters/php + cp gophermap $(DEST)/usr/lib/gophernicus/ + +# Build architecture-independent files here. +binary-indep: build install +# We have nothing to do by default. + +# Build architecture-dependent files here. +binary-arch: build install + dh_testdir + dh_testroot + dh_installchangelogs ChangeLog + dh_installdocs +# dh_installexamples +# dh_install +# dh_installmenu + dh_installdebconf + dh_installlogrotate + dh_installlogcheck +# dh_installemacsen +# dh_installpam +# dh_installmime +# dh_python +# dh_installinit +# dh_installcron +# dh_installinfo +# dh_installman + dh_link + dh_strip + dh_compress + dh_fixperms +# dh_perl +# dh_makeshlibs + dh_installdeb + dh_shlibdeps + dh_gencontrol + dh_md5sums + dh_builddeb + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install configure diff --git a/error.gif b/error.gif new file mode 100644 index 0000000..c170419 Binary files /dev/null and b/error.gif differ diff --git a/examples/counter/counter.sh b/examples/counter/counter.sh new file mode 100755 index 0000000..5742603 --- /dev/null +++ b/examples/counter/counter.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +## +## A simple visitor counter to use with gophermaps +## +## Usage: counter.sh
 
+##
+
+# Figure out a safe file to keep our counter
+HASH=`echo "$SELECTOR" | md5sum | cut -d" " -f1`
+FILE=/var/tmp/gopher-counter-$HASH
+
+# Get count and the previous visitors IP address
+COUNT="`cut -d' ' -f1 $FILE`"
+OLD_ADDR="`cut -d' ' -f2 $FILE`"
+
+# Increase counter only if the user is new
+if [ "$OLD_ADDR" != "$REMOTE_ADDR" ]; then
+	COUNT=$(( COUNT + 1 ))
+	echo "$COUNT $REMOTE_ADDR" > $FILE
+fi
+
+# Output counter message
+echo "$1$COUNT$2"
+
diff --git a/file.c b/file.c
new file mode 100644
index 0000000..466cff5
--- /dev/null
+++ b/file.c
@@ -0,0 +1,402 @@
+/*
+ * Gophernicus - Copyright (c) 2009-2012 Kim Holviala 
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#include "gophernicus.h"
+
+
+/*
+ * Send a binary file to the client
+ */
+void send_binary_file(state *st)
+{
+	/* Faster sendfile() version */
+#ifdef HAVE_SENDFILE
+	int fd;
+	off_t offset = 0;
+
+	if (st->debug) syslog(LOG_INFO, "outputting binary file \"%s\"", st->req_realpath);
+
+	if ((fd = open(st->req_realpath, O_RDONLY)) == ERROR) return;
+	sendfile(1, fd, &offset, st->req_filesize);
+	close(fd);
+
+	/* More compatible POSIX fread()/fwrite() version */
+#else
+	FILE *fp;
+	char buf[BUFSIZE];
+	int bytes;
+
+	if (st->debug) syslog(LOG_INFO, "outputting binary file \"%s\"", st->req_realpath);
+
+	if ((fp = fopen(st->req_realpath , "r")) == NULL) return;
+	while ((bytes = fread(buf, 1, sizeof(buf), fp)) > 0)
+		fwrite(buf, bytes, 1, stdout);
+	fclose(fp);
+#endif
+}
+
+
+/*
+ * Send a text file to the client
+ */
+void send_text_file(state *st)
+{
+	FILE *fp;
+	char in[BUFSIZE];
+	char out[BUFSIZE];
+	int line;
+
+	if (st->debug) syslog(LOG_INFO, "outputting text file \"%s\"", st->req_realpath);
+	if ((fp = fopen(st->req_realpath , "r")) == NULL) return;
+
+	/* Loop through the file line by line */
+	line = 0;
+
+	while (fgets(in, sizeof(in), fp)) {
+
+		/* Covert to output charset & print */
+		if (st->opt_iconv) sstrniconv(st->out_charset, out, in);
+		else sstrlcpy(out, in);
+
+		chomp(out);
+
+#ifdef ENABLE_STRICT_RFC1436
+		if (strcmp(out, ".") == MATCH) printf(".." CRLF);
+		else
+#endif
+		printf("%s" CRLF, out);
+		line++;
+	}
+
+#ifdef ENABLE_STRICT_RFC1436
+	printf("." CRLF);
+#endif
+	fclose(fp);
+}
+
+
+/*
+ * Print hURL redirect page
+ */
+void url_redirect(state *st)
+{
+	char dest[BUFSIZE];
+	char *c;
+
+	/* Basic security checking */
+	sstrlcpy(dest, st->req_selector + 4);
+
+	if (sstrncmp(dest, "http://") != MATCH &&
+	    sstrncmp(dest, "ftp://") != MATCH &&
+	    sstrncmp(dest, "mailto:") != MATCH)
+		die(st, ERR_ACCESS, "Refusing to HTTP redirect unsafe protocols");
+
+	if ((c = strchr(dest, '"'))) *c = '\0';
+	if ((c = strchr(dest, '?'))) *c = '\0';
+
+	/* Log the redirect */
+	if (st->opt_syslog) {
+		syslog(LOG_INFO, "request for \"gopher://%s:%i/h%s\" from %s",
+			st->server_host,
+			st->server_port,
+			st->req_selector,
+			st->req_remote_addr);
+	}
+	log_combined(st, HTTP_OK);
+
+	/* Output HTML */
+	printf("\n"
+		"\n\n"
+		"  \n"
+		"  \n"
+		"  URL Redirect page\n"
+		"\n\n"
+		"Redirecting to %1$s\n"
+		"
\n", dest);
+	footer(st);
+	printf("
\n\n\n"); +} + + +/* + * Handle /server-status + */ +#ifdef HAVE_SHMEM +void server_status(state *st, shm_state *shm, int shmid) +{ + struct shmid_ds shm_ds; + time_t now; + time_t uptime; + int sessions; + int i; + + /* Log the request */ + if (st->opt_syslog) { + syslog(LOG_INFO, "request for \"gopher://%s:%i/0" SERVER_STATUS "\" from %s", + st->server_host, + st->server_port, + st->req_remote_addr); + } + log_combined(st, HTTP_OK); + + /* Quit if shared memory isn't initialized yet */ + if (!shm) return; + + /* Update counters */ + shm->hits++; + shm->kbytes += 1; + + /* Get server uptime */ + now = time(NULL); + uptime = (now - shm->start_time) + 1; + + /* Get shared memory info */ + shmctl(shmid, IPC_STAT, &shm_ds); + + /* Print statistics */ + printf("Total Accesses: %li" CRLF + "Total kBytes: %li" CRLF + "Uptime: %i" CRLF + "ReqPerSec: %.3f" CRLF + "BytesPerSec: %li" CRLF + "BytesPerReq: %li" CRLF + "BusyServers: %i" CRLF + "IdleServers: 0" CRLF + "CPULoad: %.2f" CRLF, + shm->hits, + shm->kbytes, + (int) uptime, + (float) shm->hits / (float) uptime, + shm->kbytes * 1024 / (int) uptime, + shm->kbytes * 1024 / (shm->hits + 1), + (int) shm_ds.shm_nattch, + loadavg()); + + /* Print active sessions */ + sessions = 0; + + for (i = 0; i < SHM_SESSIONS; i++) { + if ((now - shm->session[i].req_atime) < st->session_timeout) { + sessions++; + + printf("Session: %-4i %-40s %-4li %-7li gopher://%s:%i/%c%s" CRLF, + (int) (now - shm->session[i].req_atime), + shm->session[i].req_remote_addr, + shm->session[i].hits, + shm->session[i].kbytes, + shm->session[i].server_host, + shm->session[i].server_port, + shm->session[i].req_filetype, + shm->session[i].req_selector); + } + } + + printf("Total Sessions: %i" CRLF, sessions); +} +#endif + + +/* + * Handle /caps.txt + */ +void caps_txt(state *st, shm_state *shm) +{ + /* Log the request */ + if (st->opt_syslog) { + syslog(LOG_INFO, "request for \"gopher://%s:%i/0" CAPS_TXT "\" from %s", + st->server_host, + st->server_port, + st->req_remote_addr); + } + log_combined(st, HTTP_OK); + + /* Update counters */ +#ifdef HAVE_SHMEM + if (shm) { + shm->hits++; + shm->kbytes += 1; + + /* Update session data */ + st->req_filesize += 1024; + update_shm_session(st, shm); + } +#endif + + /* Standard caps.txt stuff */ + printf("CAPS" CRLF + CRLF + "##" CRLF + "## This is an automatically generated caps file." CRLF + "##" CRLF + CRLF + "CapsVersion=1" CRLF + "ExpireCapsAfter=%i" CRLF + CRLF + "PathDelimeter=/" CRLF + "PathIdentity=." CRLF + "PathParent=.." CRLF + "PathParentDouble=FALSE" CRLF + "PathKeepPreDelimeter=FALSE" CRLF + CRLF + "ServerSoftware=" SERVER_SOFTWARE CRLF + "ServerSoftwareVersion=" VERSION CRLF + "ServerArchitecture=%s" CRLF, + st->session_timeout, + st->server_platform); + + /* Optional keys */ + if (*st->server_description) + printf("ServerDescription=%s" CRLF, st->server_description); + if (*st->server_location) + printf("ServerGeolocationString=%s" CRLF, st->server_location); + if (*st->server_admin) + printf("ServerAdmin=%s" CRLF, st->server_admin); +} + + +/* + * Setup environment variables as per the CGI spec + */ +void setenv_cgi(state *st, char *script) +{ + char buf[BUFSIZE]; + + /* Security */ + setenv("PATH", SAFE_PATH, 1); + + /* Set up the environment as per CGI spec */ + setenv("GATEWAY_INTERFACE", "CGI/1.1", 1); + setenv("CONTENT_LENGTH", "0", 1); + setenv("QUERY_STRING", st->req_query_string, 1); + snprintf(buf, sizeof(buf), SERVER_SOFTWARE_FULL, st->server_platform); + setenv("SERVER_SOFTWARE", buf, 1); + setenv("SERVER_ARCH", st->server_platform, 1); + setenv("SERVER_DESCRIPTION", st->server_description, 1); + snprintf(buf, sizeof(buf), SERVER_SOFTWARE "/" VERSION); + setenv("SERVER_VERSION", buf, 1); + + if (st->req_protocol == PROTO_HTTP) + setenv("SERVER_PROTOCOL", "HTTP/0.9", 1); + else + setenv("SERVER_PROTOCOL", "RFC1436", 1); + + setenv("SERVER_NAME", st->server_host, 1); + snprintf(buf, sizeof(buf), "%i", st->server_port); + setenv("SERVER_PORT", buf, 1); + setenv("REQUEST_METHOD", "GET", 1); + setenv("DOCUMENT_ROOT", st->server_root, 1); + setenv("SCRIPT_NAME", st->req_selector, 1); + setenv("SCRIPT_FILENAME", script, 1); + setenv("LOCAL_ADDR", st->req_local_addr, 1); + setenv("REMOTE_ADDR", st->req_remote_addr, 1); + setenv("HTTP_REFERER", st->req_referrer, 1); + setenv("HTTP_ACCEPT_CHARSET", strcharset(st->out_charset), 1); + + /* Gophernicus extras */ + snprintf(buf, sizeof(buf), "%c", st->req_filetype); + setenv("GOPHER_FILETYPE", buf, 1); + setenv("GOPHER_CHARSET", strcharset(st->out_charset), 1); + setenv("GOPHER_REFERER", st->req_referrer, 1); + snprintf(buf, sizeof(buf), "%i", st->out_width); + setenv("COLUMNS", buf, 1); + + /* Bucktooth extras */ + if (*st->req_query_string) { + snprintf(buf, sizeof(buf), "%s?%s", + st->req_selector, st->req_query_string); + setenv("SELECTOR", buf, 1); + } + else setenv("SELECTOR", st->req_selector, 1); + + setenv("SERVER_HOST", st->server_host, 1); + setenv("REQUEST", st->req_selector, 1); + setenv("SEARCHREQUEST", st->req_query_string, 1); +} + + +/* + * Execute a CGI script + */ +void run_cgi(state *st, char *script, char *arg) +{ + /* Setup environment & execute the binary */ + if (st->debug) syslog(LOG_INFO, "executing script \"%s\"", script); + + setenv_cgi(st, script); + execl(script, script, arg, NULL); + + /* Didn't work - die */ + die(st, ERR_ACCESS, NULL); +} + + +/* + * Handle file selectors + */ +void gopher_file(state *st) +{ + struct stat file; + char buf[BUFSIZE]; + char *c; + + /* Refuse to serve out gophermaps/tags */ + if ((c = strrchr(st->req_realpath, '/'))) c++; + else c = st->req_realpath; + + if (strcmp(c, st->map_file) == MATCH) + die(st, ERR_ACCESS, "Refusing to serve out a gophermap file"); + if (strcmp(c, st->tag_file) == MATCH) + die(st, ERR_ACCESS, "Refusing to serve out a gophertag file"); + + /* Check for & run CGI and query scripts */ + if (strstr(st->req_realpath, st->cgi_file) || st->req_filetype == TYPE_QUERY) + run_cgi(st, st->req_realpath, NULL); + + /* Check for a file suffix filter */ + if (*st->filter_dir && (c = strrchr(st->req_realpath, '.'))) { + snprintf(buf, sizeof(buf), "%s/%s", st->filter_dir, c + 1); + + /* Filter file through the script */ + if (stat(buf, &file) == OK && (file.st_mode & S_IXOTH)) + run_cgi(st, buf, st->req_realpath); + } + + /* Check for a filetype filter */ + if (*st->filter_dir) { + snprintf(buf, sizeof(buf), "%s/%c", st->filter_dir, st->req_filetype); + + /* Filter file through the script */ + if (stat(buf, &file) == OK && (file.st_mode & S_IXOTH)) + run_cgi(st, buf, st->req_realpath); + } + + /* Output regular files */ + if (st->req_filetype == TYPE_TEXT || st->req_filetype == TYPE_MIME) + send_text_file(st); + else + send_binary_file(st); +} + + diff --git a/gophermap b/gophermap new file mode 100644 index 0000000..776965f --- /dev/null +++ b/gophermap @@ -0,0 +1,36 @@ +!Welcome to Gophernicus! +# +# $ figlet -f chunky Gophernicus +# + _______ __ __ +| __|.-----.-----.| |--.-----.----.-----.|__|.----.--.--.-----. +| | || _ | _ || | -__| _| || || __| | |__ --| +|_______||_____| __||__|__|_____|__| |__|__||__||____|_____|_____| + |__| +# +# Shamelessly lifted from Apache 1.3... +# +If you can see this, it means that the installation of Gophernicus +on this system was successful. You may now add content to this +directory and replace this page. + +# +# Real-time configuration output (WOO!) +# +Generic information: +=echo " current time...: `date`" +=echo " your ip address: $REMOTE_ADDR" +=echo " server uptime..: `uptime | sed 's/.*up \([^,]*\), .*/\1/' `" +=echo " server version.: $SERVER_VERSION" +=echo " server platform: $SERVER_ARCH" +=echo " description....: $SERVER_DESCRIPTION" + +Server configuration: +=echo " config file....: `for FILE in /Library/LaunchDaemons/org.gophernicus.server.plist /boot/common/settings/network/services /etc/xinetd.d/gophernicus /etc/inetd.conf; do if [ -f $FILE ]; then echo $FILE; break; fi; done`" +=echo " server hostname: $SERVER_HOST" +=echo " root directory.: $DOCUMENT_ROOT" +=echo " running as user: `whoami`" +=echo " output charset.: $GOPHER_CHARSET" +=echo " output width...: $COLUMNS characters" + +* diff --git a/gophernicus.c b/gophernicus.c new file mode 100644 index 0000000..f2c12d2 --- /dev/null +++ b/gophernicus.c @@ -0,0 +1,735 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Print gopher menu line + */ +void info(state *st, char *str, char type) +{ + char buf[BUFSIZE]; + char selector[16]; + + /* Convert string to output charset */ + if (st->opt_iconv) sstrniconv(st->out_charset, buf, str); + else sstrlcpy(buf, str); + + /* Handle gopher title resources */ + strclear(selector); + if (type == TYPE_TITLE) { + sstrlcpy(selector, "TITLE"); + type = TYPE_INFO; + } + + /* Output info line */ + strcut(buf, st->out_width); + printf("%c%s\t%s\t%s" CRLF, + type, buf, selector, DUMMY_HOST); +} + + +/* + * Print footer + */ +void footer(state *st) +{ + char line[BUFSIZE]; + char buf[BUFSIZE]; + char msg[BUFSIZE]; + + if (!st->opt_footer) { +#ifndef ENABLE_STRICT_RFC1436 + if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY) +#endif + printf("." CRLF); + return; + } + + /* Create horizontal line */ + strrepeat(line, '_', st->out_width); + + /* Create right-aligned footer message */ + snprintf(buf, sizeof(buf), FOOTER_FORMAT, st->server_platform); + snprintf(msg, sizeof(msg), "%*s", st->out_width - 1, buf); + + /* Menu footer? */ + if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY) { + info(st, line, TYPE_INFO); + info(st, msg, TYPE_INFO); + printf("." CRLF); + } + + /* Plain text footer */ + else { + printf("%s" CRLF, line); + printf("%s" CRLF, msg); +#ifdef ENABLE_STRICT_RFC1436 + printf("." CRLF); +#endif + } +} + + +/* + * Print error message & exit + */ +void die(state *st, char *message, char *description) +{ + int en = errno; + static const char error_gif[] = ERROR_GIF; + + /* Handle NULL description */ + if (description == NULL) description = strerror(en); + + /* Log the error */ + if (st->opt_syslog) { + syslog(LOG_ERR, "error \"%s\" for request \"%s\" from %s", + description, st->req_selector, st->req_remote_addr); + } + log_combined(st, HTTP_404); + + /* Handle menu errors */ + if (st->req_filetype == TYPE_MENU || st->req_filetype == TYPE_QUERY) { + printf("3" ERROR_PREFIX "%s\tTITLE\t" DUMMY_HOST CRLF, message); + footer(st); + } + + /* Handle image errors */ + else if (st->req_filetype == TYPE_GIF || st->req_filetype == TYPE_IMAGE) { + fwrite(error_gif, sizeof(error_gif), 1, stdout); + } + + /* Handle HTML errors */ + else if (st->req_filetype == TYPE_HTML) { + printf("\n" + "\n\n" + " \n" + " " ERROR_PREFIX "%1$s\n" + "\n\n" + "" ERROR_PREFIX "%1$s\n" + "
\n", message);
+		footer(st);
+		printf("
\n\n\n"); + } + + /* Use plain text error for other filetypes */ + else { + printf(ERROR_PREFIX "%s" CRLF, message); + footer(st); + } + + /* Quit */ + exit(EXIT_FAILURE); +} + + +/* + * Apache-compatible combined logging + */ +void log_combined(state *st, int status) +{ + FILE *fp; + struct tm *ltime; + char timestr[64]; + time_t now; + + /* Try to open the logfile for appending */ + if (!*st->log_file) return; + if ((fp = fopen(st->log_file , "a")) == NULL) return; + + /* Format time */ + now = time(NULL); + ltime = localtime(&now); + strftime(timestr, sizeof(timestr), HTTP_DATE, ltime); + + /* Generate log entry */ + fprintf(fp, "%s %s:%i - [%s] \"GET %c%s HTTP/1.0\" %i %li \"%s\" \"" HTTP_USERAGENT "\"\n", + st->req_remote_addr, + st->server_host, + st->server_port, + timestr, + st->req_filetype, + st->req_selector, + status, + (long) st->req_filesize, + st->req_referrer); + fclose(fp); +} + + +/* + * Convert gopher selector to an absolute path + */ +void selector_to_path(state *st) +{ + DIR *dp; + struct dirent *dir; + struct stat file; +#ifdef HAVE_PASSWD + struct passwd *pwd; + char *path = EMPTY; + char *c; +#endif + char buf[BUFSIZE]; + int i; + + /* Handle selector rewriting */ + for (i = 0; i < st->rewrite_count; i++) { + + /* Match found? */ + if (strstr(st->req_selector, st->rewrite[i].match) == st->req_selector) { + + /* Replace match with a new string */ + snprintf(buf, sizeof(buf), "%s%s", + st->rewrite[i].replace, + st->req_selector + strlen(st->rewrite[i].match)); + + if (st->debug) { + syslog(LOG_INFO, "rewriting selector \"%s\" -> \"%s\"", + st->req_selector, buf); + } + + sstrlcpy(st->req_selector, buf); + } + } + +#ifdef HAVE_PASSWD + /* Virtual userdir (~user -> /home/user/public_gopher)? */ + if (*(st->user_dir) && sstrncmp(st->req_selector, "/~") == MATCH) { + + /* Parse userdir login name & path */; + sstrlcpy(buf, st->req_selector + 2); + if ((c = strchr(buf, '/'))) { + *c = '\0'; + path = c + 1; + } + + /* Check user validity */ + if ((pwd = getpwnam(buf)) == NULL) + die(st, ERR_NOTFOUND, "User not found"); + if (pwd->pw_uid < PASSWD_MIN_UID) + die(st, ERR_NOTFOUND, "User found but UID too low"); + + /* Generate absolute path to users own gopher root */ + snprintf(st->req_realpath, sizeof(st->req_realpath), + "%s/%s/%s", pwd->pw_dir, st->user_dir, path); + + /* Check ~public_gopher access rights */ + if (stat(st->req_realpath, &file) == ERROR) + die(st, ERR_NOTFOUND, NULL); + if ((file.st_mode & S_IROTH) == 0) + die(st, ERR_ACCESS, "~/public_gopher not world-readable"); + if (file.st_uid != pwd->pw_uid) + die(st, ERR_ACCESS, "~/ and ~/public_gopher owned by different users"); + + /* Userdirs always come from the default vhost */ + if (st->opt_vhost) + sstrlcpy(st->server_host, st->server_host_default); + return; + } +#endif + + /* Virtual hosting */ + if (st->opt_vhost) { + + /* Try looking for the selector from the current vhost */ + snprintf(st->req_realpath, sizeof(st->req_realpath), "%s/%s%s", + st->server_root, st->server_host, st->req_selector); + if (stat(st->req_realpath, &file) == OK) return; + + /* Loop through all vhosts looking for the selector */ + if ((dp = opendir(st->server_root)) == NULL) die(st, ERR_NOTFOUND, NULL); + while ((dir = readdir(dp))) { + + /* Skip .hidden dirs and . & .. */ + if (dir->d_name[0] == '.') continue; + + /* Special case - skip lost+found (don't ask) */ + if (sstrncmp(dir->d_name, "lost+found") == MATCH) continue; + + /* Generate path to the found vhost */ + snprintf(st->req_realpath, sizeof(st->req_realpath), "%s/%s%s", + st->server_root, dir->d_name, st->req_selector); + + /* Did we find the selector under this vhost? */ + if (stat(st->req_realpath, &file) == OK) { + + /* Virtual host found - update state & return */ + sstrlcpy(st->server_host, dir->d_name); + return; + } + } + closedir(dp); + } + + /* Handle normal selectors */ + snprintf(st->req_realpath, sizeof(st->req_realpath), + "%s%s", st->server_root, st->req_selector); +} + + +/* + * Get local IP address + */ +char *get_local_address(void) +{ +#ifdef HAVE_IPv4 + struct sockaddr_in addr; + socklen_t addrsize = sizeof(addr); +#endif +#ifdef HAVE_IPv6 + struct sockaddr_in6 addr6; + socklen_t addr6size = sizeof(addr6); + static char address[INET6_ADDRSTRLEN]; +#endif + char *c; + + /* Try IPv4 first */ +#ifdef HAVE_IPv4 + if (getsockname(0, (struct sockaddr *) &addr, &addrsize) == OK) { + c = inet_ntoa(addr.sin_addr); + if (strlen(c) > 0 && *c != '0') return c; + } +#endif + + /* IPv4 didn't work - try IPv6 */ +#ifdef HAVE_IPv6 + if (getsockname(0, (struct sockaddr *) &addr6, &addr6size) == OK) { + if (inet_ntop(AF_INET6, &addr6.sin6_addr, address, sizeof(address))) { + + /* Strip ::ffff: IPv4-in-IPv6 prefix */ + if (sstrncmp(address, "::ffff:") == MATCH) return (address + 7); + else return address; + } + } +#endif + + /* Nothing works... I'm out of ideas */ + return DEFAULT_ADDR; +} + + +/* + * Get remote peer IP address + */ +char *get_peer_address(void) +{ +#ifdef HAVE_IPv4 + struct sockaddr_in addr; + socklen_t addrsize = sizeof(addr); +#endif +#ifdef HAVE_IPv6 + struct sockaddr_in6 addr6; + socklen_t addr6size = sizeof(addr6); + static char address[INET6_ADDRSTRLEN]; +#endif + char *c; + + /* Are we a CGI script? */ + if ((c = getenv("REMOTE_ADDR"))) return c; + /* if ((c = getenv("REMOTE_HOST"))) return c; */ + + /* Try IPv4 first */ +#ifdef HAVE_IPv4 + if (getpeername(0, (struct sockaddr *) &addr, &addrsize) == OK) { + c = inet_ntoa(addr.sin_addr); + if (strlen(c) > 0 && *c != '0') return c; + } +#endif + + /* IPv4 didn't work - try IPv6 */ +#ifdef HAVE_IPv6 + if (getpeername(0, (struct sockaddr *) &addr6, &addr6size) == OK) { + if (inet_ntop(AF_INET6, &addr6.sin6_addr, address, sizeof(address))) { + + /* Strip ::ffff: IPv4-in-IPv6 prefix */ + if (sstrncmp(address, "::ffff:") == MATCH) return (address + 7); + else return address; + } + } +#endif + + /* Nothing works... I'm out of ideas */ + return DEFAULT_ADDR; +} + + +/* + * Initialize state struct to default/empty values + */ +void init_state(state *st) +{ + static const char *filetypes[] = { FILETYPES }; + char buf[BUFSIZE]; + char *c; + int i; + + /* Request */ + strclear(st->req_selector); + strclear(st->req_realpath); + strclear(st->req_query_string); + strclear(st->req_referrer); + sstrlcpy(st->req_local_addr, get_local_address()); + sstrlcpy(st->req_remote_addr, get_peer_address()); + /* strclear(st->req_remote_host); */ + st->req_filetype = DEFAULT_TYPE; + st->req_protocol = PROTO_GOPHER; + st->req_filesize = 0; + + /* Output */ + st->out_width = DEFAULT_WIDTH; + st->out_charset = DEFAULT_CHARSET; + + /* Settings */ + sstrlcpy(st->server_root, DEFAULT_ROOT); + sstrlcpy(st->server_host_default, DEFAULT_HOST); + + if ((c = getenv("HOSTNAME"))) + sstrlcpy(st->server_host, c); + else if ((gethostname(buf, sizeof(buf))) != ERROR) + sstrlcpy(st->server_host, buf); + + st->server_port = DEFAULT_PORT; + + st->default_filetype = DEFAULT_TYPE; + sstrlcpy(st->map_file, DEFAULT_MAP); + sstrlcpy(st->tag_file, DEFAULT_TAG); + sstrlcpy(st->cgi_file, DEFAULT_CGI); + sstrlcpy(st->user_dir, DEFAULT_USERDIR); + strclear(st->log_file); + + st->hidden_count = 0; + st->filetype_count = 0; + strclear(st->filter_dir); + st->rewrite_count = 0; + + strclear(st->server_description); + strclear(st->server_location); + strclear(st->server_platform); + strclear(st->server_admin); + + /* Session */ + st->session_timeout = DEFAULT_SESSION_TIMEOUT; + st->session_max_kbytes = DEFAULT_SESSION_MAX_KBYTES; + st->session_max_hits = DEFAULT_SESSION_MAX_HITS; + + /* Feature options */ + st->opt_vhost = TRUE; + st->opt_parent = TRUE; + st->opt_header = TRUE; + st->opt_footer = TRUE; + st->opt_date = TRUE; + st->opt_syslog = TRUE; + st->opt_magic = TRUE; + st->opt_iconv = TRUE; + st->opt_query = TRUE; + st->opt_caps = TRUE; + st->opt_shm = TRUE; + st->opt_root = TRUE; + st->debug = FALSE; + + /* Load default suffix -> filetype mappings */ + for (i = 0; filetypes[i]; i += 2) { + if (st->filetype_count < MAX_FILETYPES) { + sstrlcpy(st->filetype[st->filetype_count].suffix, filetypes[i]); + st->filetype[st->filetype_count].type = *filetypes[i + 1]; + st->filetype_count++; + } + } +} + + +/* + * Main + */ +int main(int argc, char *argv[]) +{ + struct stat file; + state st; + char self[64]; + char selector[BUFSIZE]; + char buf[BUFSIZE]; + char *dest; + char *c; +#ifdef HAVE_SHMEM + struct shmid_ds shm_ds; + shm_state *shm; + int shmid; +#endif + + /* Get the name of this binary */ + if ((c = strrchr(argv[0], '/'))) sstrlcpy(self, c + 1); + else sstrlcpy(self, argv[0]); + + /* Initialize state */ +#ifdef HAVE_LOCALES + setlocale(LC_TIME, DATE_LOCALE); +#endif + init_state(&st); + + /* Handle command line arguments */ + parse_args(&st, argc, argv); + + /* Open syslog() */ + if (st.opt_syslog) openlog(self, LOG_PID, LOG_DAEMON); + + /* Make sure the computer is turned on */ +#ifdef __HAIKU__ + if (is_computer_on() != TRUE) + die(&st, ERR_ACCESS, "Please turn on the computer first"); +#endif + + /* Refuse to run as root */ +#ifdef HAVE_PASSWD + if (st.opt_root && getuid() == 0) + die(&st, ERR_ACCESS, "Refusing to run as root"); +#endif + + /* Try to get shared memory */ +#ifdef HAVE_SHMEM + if ((shmid = shmget(SHM_KEY, sizeof(shm_state), IPC_CREAT | SHM_MODE)) == ERROR) { + + /* Getting memory failed -> delete the old allocation */ + shmctl(shmid, IPC_RMID, &shm_ds); + shm = NULL; + } + else { + /* Map shared memory */ + if ((shm = (shm_state *) shmat(shmid, (void *) 0, 0)) == (void *) ERROR) + shm = NULL; + + /* Initialize mapped shared memory */ + if (shm && shm->start_time == 0) { + shm->start_time = time(NULL); + + /* Keep server platform & description in shm */ + platform(&st); + sstrlcpy(shm->server_platform, st.server_platform); + sstrlcpy(shm->server_description, st.server_description); + } + } + + /* For debugging shared memory issues */ + if (!st.opt_shm) shm = NULL; + + /* Get server platform and description */ + if (shm) { + sstrlcpy(st.server_platform, shm->server_platform); + + if (!*st.server_description) + sstrlcpy(st.server_description, shm->server_description); + } + else +#endif + platform(&st); + + /* Read selector, remove CRLF & encodings */ + if (fgets(selector, sizeof(selector) - 1, stdin) == NULL) + selector[0] = '\0'; + + chomp(selector); + strndecode(selector, selector, sizeof(selector)); + + if (st.debug) syslog(LOG_INFO, "client sent us \"%s\"", selector); + + /* Handle hURL: redirect page */ + if (sstrncmp(selector, "URL:") == MATCH) { + st.req_filetype = TYPE_HTML; + sstrlcpy(st.req_selector, selector); + url_redirect(&st); + return OK; + } + + /* Handle gopher+ root requests (UMN gopher client is seriously borken) */ + if (sstrncmp(selector, "\t$") == MATCH) { + printf("+-1" CRLF); + printf("+INFO: 1Main menu\t\t%s\t%i" CRLF, + st.server_host, + st.server_port); + printf("+VIEWS:" CRLF " application/gopher+-menu: <512b>" CRLF); + printf("." CRLF); + + if (st.debug) syslog(LOG_INFO, "got a request for gopher+ root menu"); + return OK; + } + + /* Convert HTTP request to gopher (respond using headerless HTTP/0.9) */ + if (sstrncmp(selector, "GET ") == MATCH || + sstrncmp(selector, "POST ") == MATCH ) { + + if ((c = strchr(selector, ' '))) sstrlcpy(selector, c + 1); + if ((c = strchr(selector, ' '))) *c = '\0'; + + st.req_protocol = PROTO_HTTP; + + if (st.debug) syslog(LOG_INFO, "got HTTP request for \"%s\"", selector); + } + + /* Save default server_host & fetch session data (including new server_host) */ + sstrlcpy(st.server_host_default, st.server_host); +#ifdef HAVE_SHMEM + if (shm) get_shm_session(&st, shm); +#endif + + /* Loop through the selector, fix it & separate query_string */ + dest = st.req_selector; + if (selector[0] != '/') *dest++ = '/'; + + for (c = selector; *c;) { + + /* Skip duplicate slashes and /./ */ + while (*c == '/' && *(c + 1) == '/') c++; + if (*c == '/' && *(c + 1) == '.' && *(c + 2) == '/') c += 2; + + /* Start of a query string (either type 7 or HTTP-style)? */ + if (*c == '\t' || (st.opt_query && *c == '?')) { + sstrlcpy(st.req_query_string, c + 1); + if ((c = strchr(st.req_query_string, '\t'))) *c = '\0'; + break; + } + + /* Start of virtual host hint? */ + if (*c == ';') { + if (st.opt_vhost) sstrlcpy(st.server_host, c + 1); + + /* Skip vhost on selector */ + while (*c && *c != '\t') c++; + continue; + } + + /* Copy valid char */ + *dest++ = *c++; + } + *dest = '\0'; + + /* Deny requests for Slashdot and /../ hackers */ + if (strstr(st.req_selector, "/.")) + die(&st, ERR_ACCESS, "Refusing to serve out dotfiles"); + + /* Handle /server-status requests */ +#ifdef HAVE_SHMEM + if (sstrncmp(st.req_selector, SERVER_STATUS) == MATCH) { + if (shm) server_status(&st, shm, shmid); + return OK; + } +#endif + + /* Remove possible extra cruft from server_host */ + if ((c = strchr(st.server_host, '\t'))) *c = '\0'; + + /* Guess request filetype so we can die() with style... */ + st.req_filetype = gopher_filetype(&st, st.req_selector, FALSE); + + /* Convert seletor to path & stat() */ + selector_to_path(&st); + if (st.debug) syslog(LOG_INFO, "path to resource is \"%s\"", st.req_realpath); + + if (stat(st.req_realpath, &file) == ERROR) { + + /* Handle virtual /caps.txt requests */ + if (st.opt_caps && sstrncmp(st.req_selector, CAPS_TXT) == MATCH) { +#ifdef HAVE_SHMEM + caps_txt(&st, shm); +#else + caps_txt(&st, NULL); +#endif + return OK; + } + + /* Requested file not found - die() */ + die(&st, ERR_NOTFOUND, NULL); + } + + /* Fetch request filesize from stat() */ + st.req_filesize = file.st_size; + + /* Everyone must have read access but no write access */ + if ((file.st_mode & S_IROTH) == 0) + die(&st, ERR_ACCESS, "File or directory not world-readable"); + if ((file.st_mode & S_IWOTH) != 0) + die(&st, ERR_ACCESS, "File or directory world-writeable"); + + /* If stat said it was a dir then it's a menu */ + if ((file.st_mode & S_IFMT) == S_IFDIR) st.req_filetype = TYPE_MENU; + + /* Not a dir - let's guess the filetype again... */ + else if ((file.st_mode & S_IFMT) == S_IFREG) + st.req_filetype = gopher_filetype(&st, st.req_realpath, st.opt_magic); + + /* Menu selectors must end with a slash */ + if (st.req_filetype == TYPE_MENU && strlast(st.req_selector) != '/') + sstrlcat(st.req_selector, "/"); + + /* Change directory to wherever the resource was */ + sstrlcpy(buf, st.req_realpath); + + if ((file.st_mode & S_IFMT) != S_IFDIR) c = dirname(buf); + else c = buf; + + if (chdir(c) == ERROR) die(&st, ERR_ACCESS, NULL); + + /* Keep count of hits and data transfer */ +#ifdef HAVE_SHMEM + if (shm) { + shm->hits++; + shm->kbytes += st.req_filesize / 1024; + + /* Update user session */ + update_shm_session(&st, shm); + } +#endif + + /* Log the request */ + if (st.opt_syslog) { + syslog(LOG_INFO, "request for \"gopher://%s:%i/%c%s\" from %s", + st.server_host, + st.server_port, + st.req_filetype, + st.req_selector, + st.req_remote_addr); + } + + /* Check file type & act accordingly */ + switch (file.st_mode & S_IFMT) { + case S_IFDIR: + log_combined(&st, HTTP_OK); + gopher_menu(&st); + break; + + case S_IFREG: + log_combined(&st, HTTP_OK); + gopher_file(&st); + break; + + default: + die(&st, ERR_ACCESS, "Refusing to serve out special files"); + } + + /* Clean exit */ + return OK; +} + diff --git a/gophernicus.h b/gophernicus.h new file mode 100644 index 0000000..6ae413e --- /dev/null +++ b/gophernicus.h @@ -0,0 +1,410 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _GOPHERNICUS_H +#define _GOPHERNICUS_H + + +/* + * Features + */ +#undef ENABLE_STRICT_RFC1436 /* Follow RFC1436 to the letter */ +#undef ENABLE_AUTOHIDING /* Hide manually listed resources from generated menus */ + + +/* + * Platform configuration + */ + +/* Defaults should fit standard POSIX systems */ +#define HAVE_IPv4 /* IPv4 should work anywhere */ +#define HAVE_IPv6 /* Requires modern POSIX */ +#define HAVE_PASSWD /* For systems with passwd-like userdb */ +#define PASSWD_MIN_UID 100 /* Minimum allowed UID for ~userdirs */ +#define HAVE_LOCALES /* setlocale() and friends */ +#define HAVE_SHMEM /* Shared memory support */ +#define HAVE_UNAME /* uname() */ +#define HAVE_POPEN /* popen() */ +#undef HAVE_STRLCPY /* strlcpy() from OpenBSD */ +#undef HAVE_SENDFILE /* sendfile() in Linux & others */ + +/* Linux */ +#ifdef __linux +#undef PASSWD_MIN_UID +#define PASSWD_MIN_UID 500 +#define HAVE_SENDFILE +#endif + +/* Embedded Linux with uClibc */ +#ifdef __UCLIBC__ +lskdjf +#undef HAVE_SHMEM +#undef HAVE_PASSWD +#endif + +/* Haiku */ +#ifdef __HAIKU__ +#undef HAVE_SHMEM +#undef HAVE_PASSWD +#endif + +/* OpenBSD */ +#ifdef __OpenBSD__ +#define HAVE_STRLCPY +#endif + +/* Add other OS-specific defines here */ + +/* + * Include headers + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_SENDFILE +#include +#include +#endif + +#ifdef HAVE_LOCALES +#include +#endif + +#ifdef HAVE_SHMEM +#include +#include +#else +#define shm_state void +#endif + +#if defined(HAVE_IPv4) || defined(HAVE_IPv6) +#include +#include +#include +#endif + +#ifdef HAVE_UNAME +#include +#endif + + +/* + * Compile-time configuration + */ + +/* Common stuff */ +#define CRLF "\r\n" +#define EMPTY "" +#define PARENT ".." +#define ROOT "/" + +#define FALSE 0 +#define TRUE 1 + +#define QUIT 1 +#define OK 0 +#define ERROR -1 + +#define MATCH 0 + +/* Gopher filetypes */ +#define TYPE_TEXT '0' +#define TYPE_MENU '1' +#define TYPE_ERROR '3' +#define TYPE_GZIP '5' +#define TYPE_QUERY '7' +#define TYPE_BINARY '9' +#define TYPE_GIF 'g' +#define TYPE_HTML 'h' +#define TYPE_INFO 'i' +#define TYPE_IMAGE 'I' +#define TYPE_MIME 'M' +#define TYPE_DOC 'd' +#define TYPE_TITLE '!' + +/* Protocols */ +#define PROTO_GOPHER 'g' +#define PROTO_HTTP 'h' + +/* Charsets */ +#define AUTO 0 +#define US_ASCII 1 +#define ISO_8859_1 2 +#define UTF_8 3 + +/* HTTP protocol stuff for logging */ +#define HTTP_OK 200 +#define HTTP_404 404 +#define HTTP_DATE "%d/%b/%Y:%T %z" +#define HTTP_USERAGENT "Unknown gopher client" + +/* Defaults for settings */ +#define DEFAULT_HOST "localhost" +#define DEFAULT_PORT 70 +#define DEFAULT_TYPE TYPE_TEXT +#define DEFAULT_MAP "gophermap" +#define DEFAULT_TAG "gophertag" +#define DEFAULT_CGI "/cgi-bin/" +#define DEFAULT_USERDIR "public_gopher" +#define DEFAULT_ADDR "unknown" +#define DEFAULT_WIDTH 70 +#define DEFAULT_CHARSET US_ASCII +#define MIN_WIDTH 33 +#define MAX_WIDTH 200 + +/* Session defaults */ +#define DEFAULT_SESSION_TIMEOUT 1800 +#define DEFAULT_SESSION_MAX_KBYTES 4194304 +#define DEFAULT_SESSION_MAX_HITS 4096 + +/* Dummy values for gopher protocol */ +#define DUMMY_SELECTOR "null" +#define DUMMY_HOST "null.host\t1" + +/* Safe $PATH for exec() */ +#ifdef __HAIKU__ +#define SAFE_PATH "/boot/common/bin:/bin" +#else +#define SAFE_PATH "/usr/bin:/bin" +#endif + +/* Special requests */ +#define SERVER_STATUS "/server-status" +#define CAPS_TXT "/caps.txt" + +/* Error messages */ +#define ERR_ACCESS "Access denied!" +#define ERR_NOTFOUND "File or directory not found!" + +#define ERROR_HOST "error.host\t1" +#define ERROR_PREFIX "Error: " + +/* Strings */ +#define SERVER_SOFTWARE "Gophernicus" +#define SERVER_SOFTWARE_FULL SERVER_SOFTWARE "/" VERSION " (%s)" + +#define HEADER_FORMAT "[%s]" +#define FOOTER_FORMAT "Gophered by Gophernicus/" VERSION " on %s" + +#define UNITS "KB", "MB", "GB", "TB", "PB", NULL +#define DATE_FORMAT "%Y-%b-%d %H:%M" /* See man 3 strftime */ +#define DATE_WIDTH 17 +#define DATE_LOCALE "POSIX" + +#define USERDIR_FORMAT "~%s", pwd->pw_name /* See man 3 getpwent */ +#define VHOST_FORMAT "gopher://%s/" + +/* ISO-8859-1 to US-ASCII look-alike conversion table */ +#define ASCII \ + "E?,f..++^%S??zY" \ + " !c_*Y|$\"C?????" \ + "AAAAAAACEEEEIIII" \ + "DNOOOOO*OUUUUYTB" \ + "aaaaaaaceeeeiiii" \ + "dnooooo/ouuuuyty" + +#define UNKNOWN '?' + +/* Sizes & maximums */ +#define BUFSIZE 1024 /* Default size for string buffers */ +#define MAX_HIDDEN 32 /* Maximum number of hidden files */ +#define MAX_FILETYPES 128 /* Maximum number of suffix to filetype mappings */ +#define MAX_FILTERS 16 /* Maximum number of file filters */ +#define MAX_SDIRENT 1024 /* Maximum number of files per directory to handle */ +#define MAX_REWRITE 32 /* Maximum number of selector rewrite options */ + +/* Struct for file suffix -> gopher filetype mapping */ +typedef struct { + char suffix[15]; + char type; +} ftype; + +/* Struct for selector rewriting */ +typedef struct { + char match[BUFSIZE]; + char replace[BUFSIZE]; +} srewrite; + +/* Struct for keeping the current options & state */ +typedef struct { + + /* Request */ + char req_selector[BUFSIZE]; + char req_realpath[BUFSIZE]; + char req_query_string[BUFSIZE]; + char req_referrer[BUFSIZE]; + char req_local_addr[64]; + char req_remote_addr[64]; + char req_filetype; + char req_protocol; + off_t req_filesize; + + /* Output */ + int out_width; + int out_charset; + + /* Settings */ + char server_description[64]; + char server_location[64]; + char server_platform[64]; + char server_admin[64]; + char server_root[256]; + char server_host_default[64]; + char server_host[64]; + int server_port; + + char default_filetype; + char map_file[64]; + char tag_file[64]; + char cgi_file[64]; + char user_dir[64]; + char log_file[256]; + + char hidden[MAX_HIDDEN][256]; + int hidden_count; + + ftype filetype[MAX_FILETYPES]; + int filetype_count; + char filter_dir[64]; + + srewrite rewrite[MAX_REWRITE]; + int rewrite_count; + + /* Session */ + int session_timeout; + int session_max_kbytes; + int session_max_hits; + + /* Feature options */ + char opt_parent; + char opt_header; + char opt_footer; + char opt_date; + char opt_syslog; + char opt_magic; + char opt_iconv; + char opt_vhost; + char opt_query; + char opt_caps; + char opt_shm; + char opt_root; + char debug; +} state; + +/* Shared memory for session & accounting data */ +#ifdef HAVE_SHMEM + +#define SHM_KEY 0xbeeb0006 /* Unique identifier + struct version */ +#define SHM_MODE 0600 /* Access mode for the shared memory */ +#define SHM_SESSIONS 256 /* Max amount of user sessions to track */ + +typedef struct { + long hits; + long kbytes; + + time_t req_atime; + char req_selector[128]; + char req_remote_addr[64]; + char req_filetype; + + char server_host[64]; + int server_port; +} shm_session; + +typedef struct { + time_t start_time; + long hits; + long kbytes; + char server_platform[64]; + char server_description[64]; + shm_session session[SHM_SESSIONS]; +} shm_state; + +#endif + +/* Struct for directory sorting */ +typedef struct { + char name[128]; /* Should be 256 but we're saving stack space */ + mode_t mode; + uid_t uid; + gid_t gid; + off_t size; + time_t mtime; +} sdirent; + + +/* File suffix to gopher filetype mappings */ +#define FILETYPES \ + "txt","0","pl","0","py","0","sh","0","tcl","0","c","0","cpp","0", "h","0","log","0", \ + "conf","0","php","0","php3","0", \ + "map","1","menu","1", \ + "hqx","4", \ + "Z","5","gz","5","tgz","5","tar","5","zip","5","bz2","5","rar","5","sea","5", \ + "q","7","qry","7", \ + "iso","9","so","9","o","9","rtf","9","ttf","9","bin","9", \ + "ics","c","ical","c", \ + "gif","g", \ + "html","h","htm","h","xhtml","h","css","h","swf","h","rdf","h","rss","h","xml","h", \ + "jpg","I","jpeg","I","png","I","bmp","I","svg","I","tif","I","tiff","I", \ + "ico","I","xbm","I","xpm","I","pcx","I", \ + "mbox","M", \ + "pdf","d","ps","d","doc","d","ppt","d","xls","d","xlsx","d","docx","d","pptx","d", \ + "mp3","s","wav","s","mid","s","wma","s","flac","s","ogg","s","aiff","s","aac","s", \ + "avi",";","mp4",";","mpg",";","mov",";","qt",";","asf",";","mpv",";","m4v",";", \ + NULL, NULL + +/* + * Useful macros + */ +#define strclear(str) str[0] = '\0'; +#define sstrlcpy(dest, src) strlcpy(dest, src, sizeof(dest)) +#define sstrlcat(dest, src) strlcat(dest, src, sizeof(dest)) +#define sstrncmp(s1, s2) strncmp(s1, s2, sizeof(s2) - 1) +#define sstrncasecmp(s1, s2) strncasecmp(s1, s2, sizeof(s2) - 1) +#define sstrniconv(charset, out, in) strniconv(charset, out, in, sizeof(out)) +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#define min(a,b) (((a) < (b)) ? (a) : (b)) + +/* + * Include generated headers + */ +#include "functions.h" +#include "files.h" + +#endif + + diff --git a/gophernicus.xinetd b/gophernicus.xinetd new file mode 100644 index 0000000..f6c429c --- /dev/null +++ b/gophernicus.xinetd @@ -0,0 +1,11 @@ +# default: on +# description: Gophernicus - Modern full-featured gopher server +service gopher +{ + socket_type = stream + wait = no + user = nobody + server = /usr/sbin/in.gophernicus + server_args = -r/var/gopher -h@HOSTNAME@ + disable = no +} diff --git a/gophertag b/gophertag new file mode 100644 index 0000000..cbee75b --- /dev/null +++ b/gophertag @@ -0,0 +1 @@ +Gophernicus documentation diff --git a/install-sh b/install-sh new file mode 100755 index 0000000..6781b98 --- /dev/null +++ b/install-sh @@ -0,0 +1,520 @@ +#!/bin/sh +# install - install a program, script, or datafile + +scriptversion=2009-04-28.21; # UTC + +# This originates from X11R5 (mit/util/scripts/install.sh), which was +# later released in X11R6 (xc/config/util/install.sh) with the +# following copyright and license. +# +# Copyright (C) 1994 X Consortium +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- +# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# Except as contained in this notice, the name of the X Consortium shall not +# be used in advertising or otherwise to promote the sale, use or other deal- +# ings in this Software without prior written authorization from the X Consor- +# tium. +# +# +# FSF changes to this file are in the public domain. +# +# Calling this script install-sh is preferred over install.sh, to prevent +# `make' implicit rules from creating a file called install from it +# when there is no Makefile. +# +# This script is compatible with the BSD install script, but was written +# from scratch. + +nl=' +' +IFS=" "" $nl" + +# set DOITPROG to echo to test this script + +# Don't use :- since 4.3BSD and earlier shells don't like it. +doit=${DOITPROG-} +if test -z "$doit"; then + doit_exec=exec +else + doit_exec=$doit +fi + +# Put in absolute file names if you don't have them in your path; +# or use environment vars. + +chgrpprog=${CHGRPPROG-chgrp} +chmodprog=${CHMODPROG-chmod} +chownprog=${CHOWNPROG-chown} +cmpprog=${CMPPROG-cmp} +cpprog=${CPPROG-cp} +mkdirprog=${MKDIRPROG-mkdir} +mvprog=${MVPROG-mv} +rmprog=${RMPROG-rm} +stripprog=${STRIPPROG-strip} + +posix_glob='?' +initialize_posix_glob=' + test "$posix_glob" != "?" || { + if (set -f) 2>/dev/null; then + posix_glob= + else + posix_glob=: + fi + } +' + +posix_mkdir= + +# Desired mode of installed file. +mode=0755 + +chgrpcmd= +chmodcmd=$chmodprog +chowncmd= +mvcmd=$mvprog +rmcmd="$rmprog -f" +stripcmd= + +src= +dst= +dir_arg= +dst_arg= + +copy_on_change=false +no_target_directory= + +usage="\ +Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE + or: $0 [OPTION]... SRCFILES... DIRECTORY + or: $0 [OPTION]... -t DIRECTORY SRCFILES... + or: $0 [OPTION]... -d DIRECTORIES... + +In the 1st form, copy SRCFILE to DSTFILE. +In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. +In the 4th, create DIRECTORIES. + +Options: + --help display this help and exit. + --version display version info and exit. + + -c (ignored) + -C install only if different (preserve the last data modification time) + -d create directories instead of installing files. + -g GROUP $chgrpprog installed files to GROUP. + -m MODE $chmodprog installed files to MODE. + -o USER $chownprog installed files to USER. + -s $stripprog installed files. + -t DIRECTORY install into DIRECTORY. + -T report an error if DSTFILE is a directory. + +Environment variables override the default commands: + CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG + RMPROG STRIPPROG +" + +while test $# -ne 0; do + case $1 in + -c) ;; + + -C) copy_on_change=true;; + + -d) dir_arg=true;; + + -g) chgrpcmd="$chgrpprog $2" + shift;; + + --help) echo "$usage"; exit $?;; + + -m) mode=$2 + case $mode in + *' '* | *' '* | *' +'* | *'*'* | *'?'* | *'['*) + echo "$0: invalid mode: $mode" >&2 + exit 1;; + esac + shift;; + + -o) chowncmd="$chownprog $2" + shift;; + + -s) stripcmd=$stripprog;; + + -t) dst_arg=$2 + shift;; + + -T) no_target_directory=true;; + + --version) echo "$0 $scriptversion"; exit $?;; + + --) shift + break;; + + -*) echo "$0: invalid option: $1" >&2 + exit 1;; + + *) break;; + esac + shift +done + +if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then + # When -d is used, all remaining arguments are directories to create. + # When -t is used, the destination is already specified. + # Otherwise, the last argument is the destination. Remove it from $@. + for arg + do + if test -n "$dst_arg"; then + # $@ is not empty: it contains at least $arg. + set fnord "$@" "$dst_arg" + shift # fnord + fi + shift # arg + dst_arg=$arg + done +fi + +if test $# -eq 0; then + if test -z "$dir_arg"; then + echo "$0: no input file specified." >&2 + exit 1 + fi + # It's OK to call `install-sh -d' without argument. + # This can happen when creating conditional directories. + exit 0 +fi + +if test -z "$dir_arg"; then + trap '(exit $?); exit' 1 2 13 15 + + # Set umask so as not to create temps with too-generous modes. + # However, 'strip' requires both read and write access to temps. + case $mode in + # Optimize common cases. + *644) cp_umask=133;; + *755) cp_umask=22;; + + *[0-7]) + if test -z "$stripcmd"; then + u_plus_rw= + else + u_plus_rw='% 200' + fi + cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; + *) + if test -z "$stripcmd"; then + u_plus_rw= + else + u_plus_rw=,u+rw + fi + cp_umask=$mode$u_plus_rw;; + esac +fi + +for src +do + # Protect names starting with `-'. + case $src in + -*) src=./$src;; + esac + + if test -n "$dir_arg"; then + dst=$src + dstdir=$dst + test -d "$dstdir" + dstdir_status=$? + else + + # Waiting for this to be detected by the "$cpprog $src $dsttmp" command + # might cause directories to be created, which would be especially bad + # if $src (and thus $dsttmp) contains '*'. + if test ! -f "$src" && test ! -d "$src"; then + echo "$0: $src does not exist." >&2 + exit 1 + fi + + if test -z "$dst_arg"; then + echo "$0: no destination specified." >&2 + exit 1 + fi + + dst=$dst_arg + # Protect names starting with `-'. + case $dst in + -*) dst=./$dst;; + esac + + # If destination is a directory, append the input filename; won't work + # if double slashes aren't ignored. + if test -d "$dst"; then + if test -n "$no_target_directory"; then + echo "$0: $dst_arg: Is a directory" >&2 + exit 1 + fi + dstdir=$dst + dst=$dstdir/`basename "$src"` + dstdir_status=0 + else + # Prefer dirname, but fall back on a substitute if dirname fails. + dstdir=` + (dirname "$dst") 2>/dev/null || + expr X"$dst" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ + X"$dst" : 'X\(//\)[^/]' \| \ + X"$dst" : 'X\(//\)$' \| \ + X"$dst" : 'X\(/\)' \| . 2>/dev/null || + echo X"$dst" | + sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ + s//\1/ + q + } + /^X\(\/\/\)[^/].*/{ + s//\1/ + q + } + /^X\(\/\/\)$/{ + s//\1/ + q + } + /^X\(\/\).*/{ + s//\1/ + q + } + s/.*/./; q' + ` + + test -d "$dstdir" + dstdir_status=$? + fi + fi + + obsolete_mkdir_used=false + + if test $dstdir_status != 0; then + case $posix_mkdir in + '') + # Create intermediate dirs using mode 755 as modified by the umask. + # This is like FreeBSD 'install' as of 1997-10-28. + umask=`umask` + case $stripcmd.$umask in + # Optimize common cases. + *[2367][2367]) mkdir_umask=$umask;; + .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; + + *[0-7]) + mkdir_umask=`expr $umask + 22 \ + - $umask % 100 % 40 + $umask % 20 \ + - $umask % 10 % 4 + $umask % 2 + `;; + *) mkdir_umask=$umask,go-w;; + esac + + # With -d, create the new directory with the user-specified mode. + # Otherwise, rely on $mkdir_umask. + if test -n "$dir_arg"; then + mkdir_mode=-m$mode + else + mkdir_mode= + fi + + posix_mkdir=false + case $umask in + *[123567][0-7][0-7]) + # POSIX mkdir -p sets u+wx bits regardless of umask, which + # is incompatible with FreeBSD 'install' when (umask & 300) != 0. + ;; + *) + tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ + trap 'ret=$?; rmdir "$tmpdir/d" "$tmpdir" 2>/dev/null; exit $ret' 0 + + if (umask $mkdir_umask && + exec $mkdirprog $mkdir_mode -p -- "$tmpdir/d") >/dev/null 2>&1 + then + if test -z "$dir_arg" || { + # Check for POSIX incompatibilities with -m. + # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or + # other-writeable bit of parent directory when it shouldn't. + # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. + ls_ld_tmpdir=`ls -ld "$tmpdir"` + case $ls_ld_tmpdir in + d????-?r-*) different_mode=700;; + d????-?--*) different_mode=755;; + *) false;; + esac && + $mkdirprog -m$different_mode -p -- "$tmpdir" && { + ls_ld_tmpdir_1=`ls -ld "$tmpdir"` + test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" + } + } + then posix_mkdir=: + fi + rmdir "$tmpdir/d" "$tmpdir" + else + # Remove any dirs left behind by ancient mkdir implementations. + rmdir ./$mkdir_mode ./-p ./-- 2>/dev/null + fi + trap '' 0;; + esac;; + esac + + if + $posix_mkdir && ( + umask $mkdir_umask && + $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" + ) + then : + else + + # The umask is ridiculous, or mkdir does not conform to POSIX, + # or it failed possibly due to a race condition. Create the + # directory the slow way, step by step, checking for races as we go. + + case $dstdir in + /*) prefix='/';; + -*) prefix='./';; + *) prefix='';; + esac + + eval "$initialize_posix_glob" + + oIFS=$IFS + IFS=/ + $posix_glob set -f + set fnord $dstdir + shift + $posix_glob set +f + IFS=$oIFS + + prefixes= + + for d + do + test -z "$d" && continue + + prefix=$prefix$d + if test -d "$prefix"; then + prefixes= + else + if $posix_mkdir; then + (umask=$mkdir_umask && + $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break + # Don't fail if two instances are running concurrently. + test -d "$prefix" || exit 1 + else + case $prefix in + *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; + *) qprefix=$prefix;; + esac + prefixes="$prefixes '$qprefix'" + fi + fi + prefix=$prefix/ + done + + if test -n "$prefixes"; then + # Don't fail if two instances are running concurrently. + (umask $mkdir_umask && + eval "\$doit_exec \$mkdirprog $prefixes") || + test -d "$dstdir" || exit 1 + obsolete_mkdir_used=true + fi + fi + fi + + if test -n "$dir_arg"; then + { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && + { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && + { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || + test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 + else + + # Make a couple of temp file names in the proper directory. + dsttmp=$dstdir/_inst.$$_ + rmtmp=$dstdir/_rm.$$_ + + # Trap to clean up those temp files at exit. + trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 + + # Copy the file name to the temp name. + (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && + + # and set any options; do chmod last to preserve setuid bits. + # + # If any of these fail, we abort the whole thing. If we want to + # ignore errors from any of these, just make sure not to ignore + # errors from the above "$doit $cpprog $src $dsttmp" command. + # + { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && + { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && + { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && + { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && + + # If -C, don't bother to copy if it wouldn't change the file. + if $copy_on_change && + old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && + new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && + + eval "$initialize_posix_glob" && + $posix_glob set -f && + set X $old && old=:$2:$4:$5:$6 && + set X $new && new=:$2:$4:$5:$6 && + $posix_glob set +f && + + test "$old" = "$new" && + $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 + then + rm -f "$dsttmp" + else + # Rename the file to the real destination. + $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || + + # The rename failed, perhaps because mv can't rename something else + # to itself, or perhaps because mv is so ancient that it does not + # support -f. + { + # Now remove or move aside any old file at destination location. + # We try this two ways since rm can't unlink itself on some + # systems and the destination file might be busy for other + # reasons. In this case, the final cleanup might fail but the new + # file should still install successfully. + { + test ! -f "$dst" || + $doit $rmcmd -f "$dst" 2>/dev/null || + { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && + { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } + } || + { echo "$0: cannot unlink or rename $dst" >&2 + (exit 1); exit 1 + } + } && + + # Now rename the file to the real destination. + $doit $mvcmd "$dsttmp" "$dst" + } + fi || exit 1 + + trap '' 0 + fi +done + +# Local variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC" +# time-stamp-end: "; # UTC" +# End: diff --git a/menu.c b/menu.c new file mode 100644 index 0000000..33847e7 --- /dev/null +++ b/menu.c @@ -0,0 +1,665 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Alphabetic folders first sort for sortdir() + */ +int foldersort(const void *a, const void *b) +{ + mode_t amode; + mode_t bmode; + + amode = (*(sdirent *) a).mode & S_IFMT; + bmode = (*(sdirent *) b).mode & S_IFMT; + + if (amode == S_IFDIR && bmode != S_IFDIR) return -1; + if (amode != S_IFDIR && bmode == S_IFDIR) return 1; + + return strcmp((*(sdirent *) a).name, (*(sdirent *) b).name); +} + + +/* + * Scan, stat and sort a directory folders first (scandir replacement) + */ +int sortdir(char *path, sdirent *list, int max) +{ + DIR *dp; + struct dirent *d; + struct stat s; + char buf[BUFSIZE]; + int i; + + /* Try to open the dir */ + if ((dp = opendir(path)) == NULL) return 0; + i = 0; + + /* Loop through the directory & stat() everything */ + while (max--) { + if ((d = readdir(dp)) == NULL) break; + + snprintf(buf, sizeof(buf), "%s/%s", path, d->d_name); + if (stat(buf, &s) == ERROR) continue; + + if (strlen(d->d_name) > sizeof(list[i].name)) continue; + sstrlcpy(list[i].name, d->d_name); + + list[i].mode = s.st_mode; + list[i].uid = s.st_uid; + list[i].gid = s.st_gid; + list[i].size = s.st_size; + list[i].mtime = s.st_mtime; + i++; + } + closedir(dp); + + /* Sort the entries */ + if (i > 1) qsort(list, i, sizeof(sdirent), foldersort); + + /* Return number of entries found */ + return i; +} + + +/* + * Print a list of users with ~/public_gopher + */ +#ifdef HAVE_PASSWD +void userlist(state *st) +{ + struct passwd *pwd; + struct stat dir; + char buf[BUFSIZE]; + struct tm *ltime; + char timestr[20]; + int width; + + /* Width of filenames for fancy listing */ + width = st->out_width - DATE_WIDTH - 15; + + /* Loop through all users */ + setpwent(); + while ((pwd = getpwent())) { + + /* Skip too small uids */ + if (pwd->pw_uid < PASSWD_MIN_UID) continue; + + /* Look for a world-readable user-owned ~/public_gopher */ + snprintf(buf, sizeof(buf), "%s/%s", pwd->pw_dir, st->user_dir); + if (stat(buf, &dir) == ERROR) continue; + if ((dir.st_mode & S_IROTH) == 0) continue; + if (dir.st_uid != pwd->pw_uid) continue; + + /* Found one */ + snprintf(buf, sizeof(buf), USERDIR_FORMAT); + + if (st->opt_date) { + ltime = localtime(&dir.st_mtime); + strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); + + printf("1%-*.*s %s - \t/~%s/\t%s\t%i" CRLF, + width, width, buf, timestr, pwd->pw_name, + st->server_host, st->server_port); + } + else { + printf("1%.*s\t/~%s/\t%s\t%i" CRLF, st->out_width, buf, + pwd->pw_name, st->server_host_default, st->server_port); + } + } + + endpwent(); +} +#endif + + +/* + * Print a list of available virtual hosts + */ +void vhostlist(state *st) +{ + sdirent dir[MAX_SDIRENT]; + struct tm *ltime; + char timestr[20]; + char buf[BUFSIZE]; + int width; + int num; + int i; + + /* Scan the root dir for vhost dirs */ + num = sortdir(st->server_root, dir, MAX_SDIRENT); + if (num < 0) die(st, ERR_NOTFOUND, "WTF?"); + + /* Width of filenames for fancy listing */ + width = st->out_width - DATE_WIDTH - 15; + + /* Loop through the directory entries */ + for (i = 0; i < num; i++) { + + /* Skip dotfiles */ + if (dir[i].name[0] == '.') continue; + + /* Require FQDN */ + if (!strchr(dir[i].name, '.')) continue; + + /* We only want world-readable directories */ + if ((dir[i].mode & S_IROTH) == 0) continue; + if ((dir[i].mode & S_IFMT) != S_IFDIR) continue; + + /* Generate display string for vhost */ + snprintf(buf, sizeof(buf), VHOST_FORMAT, dir[i].name); + + /* Fancy listing */ + if (st->opt_date) { + ltime = localtime(&dir[i].mtime); + strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); + + printf("1%-*.*s %s - \t/;%s\t%s\t%i" CRLF, + width, width, buf, timestr, dir[i].name, + dir[i].name, st->server_port); + } + + /* Teh boring version */ + else { + printf("1%.*s\t/;%s\t%s\t%i" CRLF, st->out_width, buf, + dir[i].name, dir[i].name, st->server_port); + } + } +} + + +/* + * Return gopher filetype for a file + */ +char gopher_filetype(state *st, char *file, char magic) +{ + FILE *fp; + char buf[BUFSIZE]; + char *c; + int i; + + /* If it ends with an slash it's a menu */ + if (!*file) return st->default_filetype; + if (strlast(file) == '/') return TYPE_MENU; + + /* Get file suffix */ + if ((c = strrchr(file, '.'))) { + c++; + + /* Loop through the filetype array looking for a match*/ + for (i = 0; i < st->filetype_count; i++) + if (strcasecmp(st->filetype[i].suffix, c) == MATCH) + return st->filetype[i].type; + } + + /* Are we allowed to look inside files? */ + if (!magic) return st->default_filetype; + + /* Read data from the file */ + if ((fp = fopen(file , "r")) == NULL) return st->default_filetype; + i = fread(buf, 1, sizeof(buf) - 1, fp); + buf[i] = '\0'; + fclose(fp); + + /* GIF images */ + if (sstrncmp(buf, "GIF89a") == MATCH || + sstrncmp(buf, "GIF87a") == MATCH) return TYPE_GIF; + + /* JPEG images */ + if (sstrncmp(buf, "\377\330\377\340") == MATCH) return TYPE_IMAGE; + + /* PNG images */ + if (sstrncmp(buf, "\211PNG") == MATCH) return TYPE_IMAGE; + + /* mbox */ + if (strstr(buf, "\nFrom: ") && + strstr(buf, "\nSubject: ")) return TYPE_MIME; + + /* MIME */ + if (strstr(buf, "\nContent-Type: ")) return TYPE_MIME; + + /* HTML files */ + if (buf[0] == '<' && + (strstr(buf, "default_filetype; +} + + +/* + * Handle gophermaps + */ +int gophermap(state *st, char *mapfile, int depth) +{ + FILE *fp; + struct stat file; + char line[BUFSIZE]; + char *selector; + char *name; + char *host; + char *c; + char type; + int port; + int exe; + + /* Prevent include loops */ + if (depth > 4) return OK; + + /* Try to figure out whether the map is executable */ + if (stat(mapfile, &file) == OK) { + if ((file.st_mode & S_IXOTH)) exe = TRUE; + else exe = FALSE; + } + + /* As a fallback let's just feed everything to shell.. */ + else exe = TRUE; + + /* Debug output */ + if (st->debug) { + if (exe) syslog(LOG_INFO, "parsing executable gophermap \"%s\"", mapfile); + else syslog(LOG_INFO, "parsing static gophermap \"%s\"", mapfile); + } + + /* Try to execute or open the mapfile */ +#ifdef HAVE_POPEN + if (exe) { + setenv_cgi(st, mapfile); + if ((fp = popen(mapfile , "r")) == NULL) return OK; + } + else +#endif + if ((fp = fopen(mapfile , "r")) == NULL) return OK; + + /* Read lines one by one */ + while (fgets(line, sizeof(line) - 1, fp)) { + /* Parse type & name */ + chomp(line); + type = line[0]; + name = line + 1; + + /* Ignore #comments */ + if (type == '#') continue; + + /* Stop handling gophermap? */ + if (type == '*') return OK; + if (type == '.') return QUIT; + + /* Print a list of users with public_gopher */ + if (type == '~') { +#ifdef HAVE_PASSWD + userlist(st); +#endif + continue; + } + + /* Print a list of available virtual hosts */ + if (type == '%') { + if (st->opt_vhost) vhostlist(st); + continue; + } + + /* Hide files in menus */ + if (type == '-') { + if (st->hidden_count < MAX_HIDDEN) + sstrlcpy(st->hidden[st->hidden_count++], name); + continue; + } + + /* Override filetype mappings */ + if (type == ':') { + add_ftype_mapping(st, name); + continue; + } + + /* Include gophermap or shell exec */ + if (type == '=') { + gophermap(st, name, depth + 1); + continue; + } + + /* Title resource */ + if (type == TYPE_TITLE) { + info(st, name, TYPE_TITLE); + continue; + } + + /* Print out non-resources as info text */ + if (!strchr(line, '\t')) { + info(st, line, TYPE_INFO); + continue; + } + + /* Parse selector */ + selector = EMPTY; + if ((c = strchr(name, '\t'))) { + *c = '\0'; + selector = c + 1; + } + if (!*selector) selector = name; + + /* Parse host */ + host = st->server_host; + if ((c = strchr(selector, '\t'))) { + *c = '\0'; + host = c + 1; + } + + /* Parse port */ + port = st->server_port; + if ((c = strchr(host, '\t'))) { + *c = '\0'; + port = atoi(c + 1); + } + + /* Handle remote, absolute and hURL gopher resources */ + if (sstrncmp(selector, "URL:") == MATCH || + selector[0] == '/' || + host != st->server_host) { + + printf("%c%s\t%s\t%s\t%i" CRLF, type, name, + selector, host, port); + } + + /* Handle relative resources */ + else { + printf("%c%s\t%s%s\t%s\t%i" CRLF, type, name, + st->req_selector, selector, host, port); + + /* Automatically hide manually defined selectors */ +#ifdef ENABLE_AUTOHIDING + if (st->hidden_count < MAX_HIDDEN) + sstrlcpy(st->hidden[st->hidden_count++], selector); +#endif + } + } + + /* Clean up & return */ +#ifdef HAVE_POPEN + if (exe) pclose(fp); + else +#endif + fclose(fp); + + return QUIT; +} + + +/* + * Handle gopher menus + */ +void gopher_menu(state *st) +{ + FILE *fp; + sdirent dir[MAX_SDIRENT]; + struct tm *ltime; + struct stat file; + char buf[BUFSIZE]; + char pathname[BUFSIZE]; + char displayname[BUFSIZE]; + char encodedname[BUFSIZE]; + char timestr[20]; + char sizestr[20]; + char *parent; + char *c; + char type; + int width; + int num; + int i; + int n; + + /* Check for a gophermap */ + snprintf(pathname, sizeof(pathname), "%s/%s", + st->req_realpath, st->map_file); + + if (stat(pathname, &file) == OK && + (file.st_mode & S_IFMT) == S_IFREG) { + + /* Parse gophermap */ + if (gophermap(st, pathname, 0) == QUIT) { + footer(st); + return; + } + } + + else { + /* Check for a gophertag */ + snprintf(pathname, sizeof(pathname), "%s/%s", + st->req_realpath, st->tag_file); + + if (stat(pathname, &file) == OK && + (file.st_mode & S_IFMT) == S_IFREG) { + + /* Read & output gophertag */ + if ((fp = fopen(pathname , "r"))) { + + fgets(buf, sizeof(buf), fp); + chomp(buf); + + info(st, buf, TYPE_TITLE); + info(st, EMPTY, TYPE_INFO); + fclose(fp); + } + } + + /* No gophermap or tag found - print default header */ + else if (st->opt_header) { + + /* Use the selector as menu title */ + sstrlcpy(displayname, st->req_selector); + + /* Shorten too long titles */ + while (strlen(displayname) > (st->out_width - sizeof(HEADER_FORMAT))) { + if ((c = strchr(displayname, '/')) == NULL) break; + + if (!*++c) break; + sstrlcpy(displayname, c); + } + + /* Output menu title */ + snprintf(buf, sizeof(buf), HEADER_FORMAT, displayname); + info(st, buf, TYPE_TITLE); + info(st, EMPTY, TYPE_INFO); + } + } + + /* Scan the directory */ + num = sortdir(st->req_realpath, dir, MAX_SDIRENT); + if (num < 0) die(st, ERR_NOTFOUND, "WTF?"); + + /* Create link to parent directory */ + if (st->opt_parent) { + sstrlcpy(buf, st->req_selector); + parent = dirname(buf); + + /* Root has no parent */ + if (strcmp(st->req_selector, ROOT) != MATCH) { + + /* Prevent double-slash */ + if (strcmp(parent, ROOT) == MATCH) parent++; + + /* Print link */ + printf("1%-*s\t%s/\t%s\t%i" CRLF, + st->opt_date ? (st->out_width - 1) : (int) strlen(PARENT), + PARENT, parent, st->server_host, st->server_port); + } + } + + /* Width of filenames for fancy listing */ + width = st->out_width - DATE_WIDTH - 15; + + /* Loop through the directory entries */ + for (i = 0; i < num; i++) { + + /* Get full path+name */ + snprintf(pathname, sizeof(pathname), "%s/%s", + st->req_realpath, dir[i].name); + + /* Skip dotfiles and non world-readables */ + if (dir[i].name[0] == '.') continue; + if ((dir[i].mode & S_IROTH) == 0) continue; + + /* Skip gophermaps and tags (but not dirs) */ + if ((dir[i].mode & S_IFMT) != S_IFDIR) { + if (strcmp(dir[i].name, st->map_file) == MATCH) continue; + if (strcmp(dir[i].name, st->tag_file) == MATCH) continue; + } + + /* Skip files marked for hiding */ + for (n = 0; n < st->hidden_count; n++) + if (strcmp(dir[i].name, st->hidden[n]) == MATCH) break; + if (n < st->hidden_count) continue; /* Cruel hack... */ + + /* Generate display name with correct output charset */ + if (st->opt_iconv) + sstrniconv(st->out_charset, displayname, dir[i].name); + else + sstrlcpy(displayname, dir[i].name); + + /* #OCT-encode filename */ + strnencode(encodedname, dir[i].name, sizeof(encodedname)); + + /* Handle inline .gophermap */ + if (strstr(displayname, st->map_file) > displayname) { + gophermap(st, pathname, 0); + continue; + } + + /* Handle directories */ + if ((dir[i].mode & S_IFMT) == S_IFDIR) { + + /* Check for a gophertag */ + snprintf(buf, sizeof(buf), "%s/%s", + pathname, st->tag_file); + + if (stat(buf, &file) == OK && + (file.st_mode & S_IFMT) == S_IFREG) { + + /* Use the gophertag as displayname */ + if ((fp = fopen(buf , "r"))) { + + fgets(buf, sizeof(buf), fp); + chomp(buf); + fclose(fp); + + /* Skip empty gophertags */ + if (*buf) { + + /* Convert to output charset */ + if (st->opt_iconv) sstrniconv(st->out_charset, displayname, buf); + else sstrlcpy(displayname, buf); + } + + } + } + + /* Dir listing with dates */ + if (st->opt_date) { + ltime = localtime(&dir[i].mtime); + strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); + + /* Hack to get around UTF-8 byte != char */ + n = width - strcut(displayname, width); + strrepeat(buf, ' ', n); + + printf("1%s%s %s - \t%s%s/\t%s\t%i" CRLF, + displayname, + buf, + timestr, + st->req_selector, + encodedname, + st->server_host, + st->server_port); + } + + /* Regular dir listing */ + else { + strcut(displayname, st->out_width); + printf("1%s\t%s%s/\t%s\t%i" CRLF, + displayname, + st->req_selector, + encodedname, + st->server_host, + st->server_port); + } + + continue; + } + + /* Skip special files (sockets, fifos etc) */ + if ((dir[i].mode & S_IFMT) != S_IFREG) continue; + + /* Get file type */ + type = gopher_filetype(st, pathname, st->opt_magic); + + /* File listing with dates & sizes */ + if (st->opt_date) { + ltime = localtime(&dir[i].mtime); + strftime(timestr, sizeof(timestr), DATE_FORMAT, ltime); + strfsize(sizestr, dir[i].size, sizeof(sizestr)); + + /* Hack to get around UTF-8 byte != char */ + n = width - strcut(displayname, width); + strrepeat(buf, ' ', n); + + printf("%c%s%s %s %s\t%s%s\t%s\t%i" CRLF, type, + displayname, + buf, + timestr, + sizestr, + st->req_selector, + encodedname, + st->server_host, + st->server_port); + } + + /* Regular file listing */ + else { + strcut(displayname, st->out_width); + printf("%c%s\t%s%s\t%s\t%i" CRLF, type, + displayname, + st->req_selector, + encodedname, + st->server_host, + st->server_port); + } + } + + /* Print footer */ + footer(st); +} + diff --git a/options.c b/options.c new file mode 100644 index 0000000..86f64d2 --- /dev/null +++ b/options.c @@ -0,0 +1,186 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Add one suffix->filetype mapping to the filetypes array + */ +void add_ftype_mapping(state *st, char *suffix) +{ + char *type; + int i; + + /* Let's not do anything stupid */ + if (!*suffix) return; + if (!(type = strchr(suffix, '='))) return; + + /* Extract type from the suffix=X string */ + *type++ = '\0'; + if (!*type) return; + + /* Loop through the filetype array */ + for (i = 0; i < st->filetype_count; i++) { + + /* Old entry found? */ + if (strcasecmp(st->filetype[i].suffix, suffix) == MATCH) { + st->filetype[i].type = *type; + return; + } + } + + /* No old entry found - add new entry */ + if (i < MAX_FILETYPES) { + sstrlcpy(st->filetype[i].suffix, suffix); + st->filetype[i].type = *type; + st->filetype_count++; + } +} + + +/* + * Add one selector rewrite mapping to the array + */ +void add_rewrite_mapping(state *st, char *match) +{ + char *replace; + + /* Check input and split it into match & replace */ + if (!*match) return; + if (!(replace = strchr(match, '='))) return; + + *replace++ = '\0'; + if (!*replace) return; + + /* Insert match/replace values into the array */ + if (st->rewrite_count < MAX_REWRITE) { + sstrlcpy(st->rewrite[st->rewrite_count].match, match); + sstrlcpy(st->rewrite[st->rewrite_count].replace, replace); + st->rewrite_count++; + } +} + + +/* + * Parse command-line arguments + */ +void parse_args(state *st, int argc, char *argv[]) +{ + FILE *fp; + static const char readme[] = README; + static const char license[] = LICENSE; + struct stat file; + char buf[BUFSIZE]; + int opt; + + /* Parse args */ + while ((opt = getopt(argc, argv, "h:p:r:t:g:a:c:u:m:l:w:o:s:i:k:f:e:R:D:L:A:P:n:db?-")) != ERROR) { + switch(opt) { + case 'h': sstrlcpy(st->server_host, optarg); break; + case 'p': st->server_port = atoi(optarg); break; + case 'r': sstrlcpy(st->server_root, optarg); break; + case 't': st->default_filetype = *optarg; break; + case 'g': sstrlcpy(st->map_file, optarg); break; + case 'a': sstrlcpy(st->map_file, optarg); break; + case 'c': sstrlcpy(st->cgi_file, optarg); break; + case 'u': sstrlcpy(st->user_dir, optarg); break; + case 'm': /* obsolete, replaced by -l */ + case 'l': sstrlcpy(st->log_file, optarg); break; + + case 'w': st->out_width = atoi(optarg); break; + case 'o': + if (sstrncasecmp(optarg, "UTF-8") == MATCH) st->out_charset = UTF_8; + if (sstrncasecmp(optarg, "ISO-8859-1") == MATCH) st->out_charset = ISO_8859_1; + break; + + case 's': st->session_timeout = atoi(optarg); break; + case 'i': st->session_max_kbytes = abs(atoi(optarg)); break; + case 'k': st->session_max_hits = abs(atoi(optarg)); break; + + case 'f': sstrlcpy(st->filter_dir, optarg); break; + case 'e': add_ftype_mapping(st, optarg); break; + + case 'R': add_rewrite_mapping(st, optarg); break; + case 'D': sstrlcpy(st->server_description, optarg); break; + case 'L': sstrlcpy(st->server_location, optarg); break; + case 'A': sstrlcpy(st->server_admin, optarg); break; + + case 'n': + if (*optarg == 'v') { st->opt_vhost = FALSE; break; } + if (*optarg == 'l') { st->opt_parent = FALSE; break; } + if (*optarg == 'h') { st->opt_header = FALSE; break; } + if (*optarg == 'f') { st->opt_footer = FALSE; break; } + if (*optarg == 'd') { st->opt_date = FALSE; break; } + if (*optarg == 'c') { st->opt_magic = FALSE; break; } + if (*optarg == 'o') { st->opt_iconv = FALSE; break; } + if (*optarg == 'q') { st->opt_query = FALSE; break; } + if (*optarg == 's') { st->opt_syslog = FALSE; break; } + if (*optarg == 'a') { st->opt_caps = FALSE; break; } + if (*optarg == 'm') { st->opt_shm = FALSE; break; } + if (*optarg == 'r') { st->opt_root = FALSE; break; } + break; + + case 'd': st->debug = TRUE; break; + case 'b': puts(license); exit(EXIT_SUCCESS); + default : puts(readme); exit(EXIT_SUCCESS); + } + } + + /* Sanitize options */ + if (st->out_width > MAX_WIDTH) st->out_width = MAX_WIDTH; + if (st->out_width < MIN_WIDTH) st->out_width = MIN_WIDTH; + if (st->out_width < MIN_WIDTH + DATE_WIDTH) st->opt_date = FALSE; + if (!st->opt_syslog) st->debug = FALSE; + + /* Primary vhost directory must exist or we disable vhosting */ + if (st->opt_vhost) { + snprintf(buf, sizeof(buf), "%s/%s", st->server_root, st->server_host); + if (stat(buf, &file) == ERROR) st->opt_vhost = FALSE; + } + + /* If -D arg looks like a file load the file contents */ + if (*st->server_description == '/') { + + if ((fp = fopen(st->server_description , "r"))) { + fgets(st->server_description, sizeof(st->server_description), fp); + chomp(st->server_description); + fclose(fp); + } + else strclear(st->server_description); + } + + /* If -L arg looks like a file load the file contents */ + if (*st->server_location == '/') { + + if ((fp = fopen(st->server_location , "r"))) { + fgets(st->server_location, sizeof(st->server_location), fp); + chomp(st->server_location); + fclose(fp); + } + else strclear(st->server_location); + } +} + diff --git a/org.gophernicus.server.plist b/org.gophernicus.server.plist new file mode 100644 index 0000000..70ba442 --- /dev/null +++ b/org.gophernicus.server.plist @@ -0,0 +1,29 @@ + + + + + Label + org.gophernicus.server + ProgramArguments + + /usr/sbin/in.gophernicus + -h@HOSTNAME@ + -r/Library/GopherServer + + Sockets + + Listeners + + SockServiceName + gopher + + + UserName + nobody + inetdCompatibility + + Wait + + + + diff --git a/platform.c b/platform.c new file mode 100644 index 0000000..3247cfc --- /dev/null +++ b/platform.c @@ -0,0 +1,277 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Get OS name, version & architecture we're running on + */ +void platform(state *st) +{ +#ifdef HAVE_UNAME +#if defined(_AIX) || defined(__linux) || defined(__APPLE__) + FILE *fp; +#endif +#if defined(__arm__) || defined(__mips__) || defined(__APPLE__) + char buf[BUFSIZE]; +#endif +#ifdef __linux + struct stat file; +#endif + struct utsname name; + char sysname[64]; + char release[64]; + char machine[64]; + char *c; + + /* Fetch system information */ + uname(&name); + + strclear(sysname); + strclear(release); + strclear(machine); + + /* AIX-specific */ +#ifdef _AIX + + /* Fix uname() results */ + sstrlcpy(machine, "powerpc"); + snprintf(release, sizeof(release), "%s.%s", + name.version, + name.release); + + /* Get CPU type */ + if ((fp = popen("/usr/sbin/getsystype -i", "r"))) { + fgets(machine, sizeof(machine), fp); + pclose(fp); + + strreplace(machine, ' ', '_'); + chomp(machine); + } + + /* Get hardware name using shell uname */ + if (!*st->server_description && + (fp = popen("/usr/bin/uname -M", "r"))) { + + fgets(st->server_description, + sizeof(st->server_description), fp); + pclose(fp); + + strreplace(st->server_description, ',', ' '); + chomp(st->server_description); + } +#endif + + /* Mac OS X, just like Unix but totally different... */ +#ifdef __APPLE__ + + /* Hardcode OS name */ + sstrlcpy(sysname, "MacOSX"); + + /* Get OS X version */ + if ((fp = popen("/usr/bin/sw_vers -productVersion", "r"))) { + fgets(release, sizeof(release), fp); + chomp(release); + pclose(fp); + } + + /* Get hardware name */ + if (!*st->server_description && + (fp = popen("/usr/sbin/sysctl -n hw.model", "r"))) { + + /* Read hardware name */ + fgets(buf, sizeof(buf), fp); + pclose(fp); + + /* Clones are gone now so we'll hardcode the manufacturer */ + sstrlcpy(st->server_description, "Apple "); + sstrlcat(st->server_description, buf); + + /* Remove hardware revision */ + for (c = st->server_description; *c; c++) + if (*c >= '0' && *c <= '9') { *c = '\0'; break; } + } +#endif + + /* Linux uname() just says Linux/2.6 - let's dig deeper... */ +#ifdef __linux + + /* Most Linux ARM/MIPS boards have hardware name in /proc/cpuinfo */ +#if defined(__arm__) || defined(__mips__) + if (!*st->server_description && (fp = fopen("/proc/cpuinfo" , "r"))) { + + while (fgets(buf, sizeof(buf), fp)) { +#ifdef __arm__ + if ((c = strkey(buf, "Hardware"))) { +#else + if ((c = strkey(buf, "machine"))) { +#endif + sstrlcpy(st->server_description, c); + chomp(st->server_description); + break; + } + } + fclose(fp); + } +#endif + + /* Identify RedHat */ + if (!*sysname && (fp = fopen("/etc/redhat-release", "r"))) { + fgets(sysname, sizeof(sysname), fp); + fclose(fp); + + if ((c = strstr(sysname, "release "))) sstrlcpy(release, c + 8); + if ((c = strchr(release, ' '))) *c = '\0'; + + if ((c = strchr(sysname, ' '))) *c = '\0'; + if (strcmp(sysname, "Red") == MATCH) sstrlcpy(sysname, "RedHat"); + } + + /* Identify Slackware */ + if (!*sysname && (fp = fopen("/etc/slackware-version", "r"))) { + fgets(sysname, sizeof(sysname), fp); + fclose(fp); + + if ((c = strchr(sysname, ' '))) { + sstrlcpy(release, c + 1); + *c = '\0'; + } + } + + /* Uh-oh.... how about a standard Linux with lsb_release? */ + if (stat("/usr/bin/lsb_release", &file) == OK && (file.st_mode & S_IXOTH)) { + + if (!*sysname && (fp = popen("/usr/bin/lsb_release -i -s", "r"))) { + fgets(sysname, sizeof(sysname), fp); + chomp(sysname); + pclose(fp); + } + + if (!*release && (fp = popen("/usr/bin/lsb_release -r -s", "r"))) { + fgets(release, sizeof(release), fp); + chomp(release); + pclose(fp); + } + } + + /* OK, nothing worked - let's try /etc/issue for sysname */ + if (!*sysname && (fp = fopen("/etc/issue", "r"))) { + fgets(sysname, sizeof(sysname), fp); + fclose(fp); + + if ((c = strchr(sysname, ' '))) *c = '\0'; + if ((c = strchr(sysname, '\\'))) *c = '\0'; + chomp(sysname); + } + + /* Debian version should be in /etc/debian_version */ + if (!*release && (fp = fopen("/etc/debian_version", "r"))) { + fgets (release, sizeof(release), fp); + fclose(fp); + + if ((c = strchr(release, '/'))) *c = '\0'; + chomp(release); + } +#endif + + /* Haiku OS */ +#ifdef __HAIKU__ + + /* Fix release name */ + snprintf(release, sizeof(release), "R%s", name.release); +#endif + + /* Fill in the blanks using uname() data */ + if (!*sysname) sstrlcpy(sysname, name.sysname); + if (!*release) sstrlcpy(release, name.release); + if (!*machine) sstrlcpy(machine, name.machine); + + /* We're only interested in major.minor version */ + if ((c = strchr(release, '.'))) if ((c = strchr(c + 1, '.'))) *c = '\0'; + if ((c = strchr(release, '-'))) *c = '\0'; + if ((c = strchr(release, '/'))) *c = '\0'; + + /* Create a nicely formatted platform string */ + snprintf(st->server_platform, sizeof(st->server_platform), "%s/%s %s", + sysname, +#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__NetBSD__) + machine, + release); +#else + release, + machine); +#endif + + /* Debug */ + if (st->debug) { + syslog(LOG_INFO, "generated platform string \"%s\"", + st->server_platform); + } + +#else + /* Fallback reply */ + sstrlcpy(st->server_platform, "Unknown computer-like system"); +#endif +} + + +/* + * Return current CPU load + */ +float loadavg(void) +{ + FILE *fp; + char buf[BUFSIZE]; + + /* Faster Linux version */ +#ifdef __linux + buf[0] = '\0'; + if ((fp = fopen("/proc/loadavg" , "r")) == NULL) return 0; + fgets(buf, sizeof(buf), fp); + fclose(fp); + + return (float) atof(buf); + + /* Generic slow version - parse the output of uptime */ +#else +#ifdef HAVE_POPEN + char *c; + + if ((fp = popen("/usr/bin/uptime", "r"))) { + fgets(buf, sizeof(buf), fp); + pclose(fp); + + if ((c = strstr(buf, "average: ")) || (c = strstr(buf, "averages: "))) + return (float) atof(c + 10); + } +#endif + + /* Fallback reply */ + return 0; +#endif +} + + diff --git a/session.c b/session.c new file mode 100644 index 0000000..9497d0d --- /dev/null +++ b/session.c @@ -0,0 +1,144 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Locate shared memory session ID + */ +#ifdef HAVE_SHMEM +int get_shm_session_id(state *st, shm_state *shm) +{ + time_t now; + int i; + + /* Get current time */ + now = time(NULL); + + /* Locate user's old session using remote_addr */ + for (i = 0; i < SHM_SESSIONS; i++) { + if (strcmp(st->req_remote_addr, shm->session[i].req_remote_addr) == MATCH && + (now - shm->session[i].req_atime) < st->session_timeout) break; + } + + /* Return -1 on error */ + if (i == SHM_SESSIONS) return ERROR; + else return i; +} +#endif + + +/* + * Get shared memory session data + */ +#ifdef HAVE_SHMEM +void get_shm_session(state *st, shm_state *shm) +{ + int i; + + /* Get session id */ + if ((i = get_shm_session_id(st, shm)) == ERROR) return; + + /* Get session data */ + if (st->opt_vhost) { + sstrlcpy(st->server_host, shm->session[i].server_host); + st->server_port = shm->session[i].server_port; + } +} +#endif + + +/* + * Update shared memory session data + */ +#ifdef HAVE_SHMEM +void update_shm_session(state *st, shm_state *shm) +{ + time_t now; + char buf[BUFSIZE]; + int delay; + int i; + + /* Get current time */ + now = time(NULL); + + /* No existing session found? */ + if ((i = get_shm_session_id(st, shm)) == ERROR) { + + /* Look for an empty/expired session slot */ + for (i = 0; i < SHM_SESSIONS; i++) { + + if ((now - shm->session[i].req_atime) > st->session_timeout) { + + /* Found slot -> initialize it */ + sstrlcpy(shm->session[i].req_remote_addr, st->req_remote_addr); + shm->session[i].hits = 0; + shm->session[i].kbytes = 0; + break; + } + } + } + + /* No available session slot found? */ + if (i == SHM_SESSIONS) return; + + /* Get referrer from old session data */ + if (*shm->session[i].server_host) { + snprintf(buf, sizeof(buf), "gopher://%s:%i/%c%s", + shm->session[i].server_host, + shm->session[i].server_port, + shm->session[i].req_filetype, + shm->session[i].req_selector); + sstrlcpy(st->req_referrer, buf); + } + + /* Update session data */ + sstrlcpy(shm->session[i].server_host, st->server_host); + shm->session[i].server_port = st->server_port; + + sstrlcpy(shm->session[i].req_selector, st->req_selector); + shm->session[i].req_filetype = st->req_filetype; + shm->session[i].req_atime = now; + + shm->session[i].hits++; + shm->session[i].kbytes += st->req_filesize / 1024; + + /* Transfer limits exceeded? */ + if ((st->session_max_kbytes && shm->session[i].kbytes > st->session_max_kbytes) || + (st->session_max_hits && shm->session[i].hits > st->session_max_hits)) { + + /* Calculate throttle delay */ + delay = max(shm->session[i].kbytes / st->session_max_kbytes, + shm->session[i].hits / st->session_max_hits); + + /* Throttle user */ + syslog(LOG_INFO, "throttling user from %s for %i seconds", + st->req_remote_addr, delay); + sleep(delay); + } +} +#endif + diff --git a/string.c b/string.c new file mode 100644 index 0000000..ebc6fe5 --- /dev/null +++ b/string.c @@ -0,0 +1,416 @@ +/* + * Gophernicus - Copyright (c) 2009-2012 Kim Holviala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#include "gophernicus.h" + + +/* + * Repeat a character num times and zero-terminate + */ +void strrepeat(char *dest, char c, size_t num) +{ + memset(dest, c, num); + dest[num] = '\0'; +} + + +/* + * Replace characters in-place + */ +void strreplace(char *str, char from, char to) +{ + while (*str) { + if (*str == from) *str = to; + str++; + } +} + + +/* + * Cut string to width, return resulting width (UTF-8 aware) + */ +int strcut(char *str, size_t width) +{ + unsigned char c; + int w = 0; + int i; + + while (width-- && (c = *str++)) { + if (c >= 0x80 && (*str & 0xc0) == 0x80) { + i = 0; + + if ((c & 0xf8) == 0xf0) i = 3; + else if ((c & 0xf0) == 0xe0) i = 2; + else if ((c & 0xe0) == 0xc0) i = 1; + + while (i--) if (!*str++) break; + } + + w++; + } + + *str = '\0'; + return w; +} + + +/* + * Match key and return value (key: value) + */ +char *strkey(char *header, char *key) +{ + char *c; + size_t len; + + if ((len = strlen(key)) == 0) return NULL; + + if (strncasecmp(header, key, len) == MATCH) { + c = header + len; + do { c++; } while (*c == ' ' || *c == '\t'); + + if (*c != ':') return NULL; + + do { c++; } while (*c == ' ' || *c == '\t'); + return c; + } + + return NULL; +} + + +/* + * Return last character of a string + */ +char strlast(char *str) +{ + int len; + + if ((len = (int)strlen(str) - 1) >= 0) return str[len]; + else return 0; +} + + +/* + * Remove CRLF from a string + */ +void chomp(char *str) +{ + char *c; + + if ((c = strrchr(str, '\n'))) *c = '\0'; + if ((c = strrchr(str, '\r'))) *c = '\0'; +} + + +/* + * Return charset name + */ +char *strcharset(int charset) +{ + if (charset == AUTO) return "auto"; + if (charset == US_ASCII) return "US-ASCII"; + if (charset == ISO_8859_1) return "ISO-8859-1"; + if (charset == UTF_8) return "UTF-8"; + + return "(unknown)"; +} + + +/* + * Convert a string between UTF-8, ISO-8859-1 and US-ASCII + */ +void strniconv(int charset, char *out, char *in, size_t outsize) +{ + char ascii[] = ASCII; + unsigned long c; + size_t len; + int i; + + /* Loop through the input string */ + len = strlen(in); + while (--outsize && len > 0) { + + /* Get one input char */ + c = (unsigned char) *in++; + len--; + + /* 7-bit chars are the same in all three charsets */ + if (c < 0x80) { + *out++ = (unsigned char) c; + continue; + } + + /* Assume ISO-8859-1 which requires 0 extra bytes */ + i = 0; + + /* UTF-8? (We'll actually check the next char here, not current) */ + if ((*in & 0xc0) == 0x80) { + + /* Four-byte UTF-8? */ + if ((c & 0xf8) == 0xf0 && len >= 3) { c &= 0x07; i = 3; } + + /* Three-byte UTF-8? */ + else if ((c & 0xf0) == 0xe0 && len >= 2) { c &= 0x0f; i = 2; } + + /* Two-byte UTF-8? */ + else if ((c & 0xe0) == 0xc0 && len >= 1) { c &= 0x1f; i = 1; } + + /* Parse rest of the UTF-8 bytes */ + while (i--) { + c <<= 6; + c |= *in++ & 0x3f; + len--; + } + } + + /* + * At this point we've got one 32bit UTF character in c and + * we're ready to convert it to the specified output charset + */ + + /* Handle UTF-8 */ + if (charset == UTF_8) { + i = 0; + + /* Two-byte encoding? */ + if (c < 0x800 && outsize > 2) { *out++ = (c >> 6) | 0xc0; i = 1; } + + /* Three-byte encoding? */ + else if (c < 0x10000 && outsize > 3) { *out++ = (c >> 12) | 0xe0; i = 2; } + + /* Four-byte encoding? */ + else if (c < 0x110000 && outsize > 4) { *out++ = (c >> 18) | 0xf0; i = 3; } + + /* Encode rest of the UTF-8 bytes */ + while (i--) { + *out++ = ((c >> (i * 6)) & 0x3f) | 0x80; + outsize--; + } + continue; + } + + /* Handle ISO-8859-1 */ + if (charset == ISO_8859_1) { + + if (c >= 0xa0 && c <= 0xff) + *out++ = (unsigned char) c; + else + *out++ = UNKNOWN; + continue; + } + + /* Handle all other charsets as 7-bit US-ASCII */ + if (c >= 0x80 && c <= 0xff) + *out++ = ascii[c - 0x80]; + else + *out++ = UNKNOWN; + } + + /* Zero-terminate output */ + *out = '\0'; +} + + +/* + * Encode string with #OCT encoding + */ +void strnencode(char *out, const char *in, size_t outsize) +{ + unsigned char c; + + /* Loop through the input string */ + while (--outsize) { + + /* End of source? */ + if (!(c = *in++)) break; + + /* Need to encode the char? */ + if (c < '+' || c > '~') { + + /* Can we fit the encoded version into outbuffer? */ + if (outsize < 5) break; + + /* Output encoded char */ + snprintf(out, outsize, "#%.3o", c); + out += 4; + } + + /* Copy regular chars */ + else *out++ = c; + } + + /* Zero-terminate output */ + *out = '\0'; +} + + +/* + * Decode both %HEX and #OCT encodings + */ +void strndecode(char *out, char *in, size_t outsize) +{ + unsigned char c; + unsigned int i; + + /* Loop through the input string */ + while (--outsize) { + + /* End of source? */ + if (!(c = *in++)) break; + + /* Parse %hex encoding */ + if (c == '%' && strlen(in) >= 2) { + sscanf(in, "%2x", &i); + *out++ = i; + in += 2; + continue; + } + + /* Parse #octal encoding */ + if (c == '#' && strlen(in) >= 3) { + sscanf(in, "%3o", &i); + *out++ = i; + in += 3; + continue; + } + + /* Copy non-encoded chars */ + *out++ = c; + } + + /* Zero-terminate output */ + *out = '\0'; +} + + +/* + * Format number to human-readable filesize with unit + */ +void strfsize(char *out, off_t size, size_t outsize) +{ + static char *unit[] = { UNITS }; + int u; + float s; + + /* Start with kilobytes */ + s = ((float) size) / 1024; + u = 0; + + /* Loop through the units until the size is small enough */ + while (s >= 1000 && unit[(u + 1)]) { + s = s / 1024; + u++; + } + + /* Format size */ + snprintf(out, outsize, "%7.1f %s", s, unit[u]); +} + + +#ifndef HAVE_STRLCPY +/* + * Copyright (c) 1998 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +/* + * Copy src to string dst of size siz. At most siz-1 characters + * will be copied. Always NUL terminates (unless siz == 0). + * Returns strlen(src); if retval >= siz, truncation occurred. + */ +size_t strlcpy(char *dst, const char *src, size_t siz) +{ + char *d = dst; + const char *s = src; + size_t n = siz; + + /* Copy as many bytes as will fit */ + if (n != 0) { + while (--n != 0) { + if ((*d++ = *s++) == '\0') + break; + } + } + + /* Not enough room in dst, add NUL and traverse rest of src */ + if (n == 0) { + if (siz != 0) + *d = '\0'; /* NUL-terminate dst */ + while (*s++) + ; + } + + return(s - src - 1); /* count does not include NUL */ +} + +/* + * Appends src to string dst of size siz (unlike strncat, siz is the + * full size of dst, not space left). At most siz-1 characters + * will be copied. Always NUL terminates (unless siz <= strlen(dst)). + * Returns strlen(src) + MIN(siz, strlen(initial dst)). + * If retval >= siz, truncation occurred. + */ +size_t strlcat(char *dst, const char *src, size_t siz) +{ + char *d = dst; + const char *s = src; + size_t n = siz; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end */ + while (n-- != 0 && *d != '\0') + d++; + dlen = d - dst; + n = siz - dlen; + + if (n == 0) + return(dlen + strlen(s)); + while (*s != '\0') { + if (n != 1) { + *d++ = *s; + n--; + } + s++; + } + *d = '\0'; + + return(dlen + (s - src)); /* count does not include NUL */ +} + +#endif