From d829e6a347b28610a1f10f3a27bd8c804f5babb7 Mon Sep 17 00:00:00 2001 From: Kim Holviala Date: Sat, 25 Jan 2014 11:21:40 +0200 Subject: [PATCH] Initial git commit with version 1.4 --- ChangeLog | 251 ++++++++ INSTALL | 113 ++++ LICENSE | 22 + Makefile | 248 ++++++++ README | 237 +++++++ README.Gophermap | 109 ++++ TODO | 10 + bin2c.c | 67 ++ debian/changelog | 37 ++ debian/compat | 1 + debian/control | 16 + debian/copyright | 22 + debian/dirs | 4 + debian/docs | 5 + debian/gophernicus.config | 14 + debian/gophernicus.logcheck.ignore.server | 3 + debian/gophernicus.logrotate | 7 + debian/gophernicus.templates | 4 + debian/postinst | 37 ++ debian/postrm | 8 + debian/prerm | 6 + debian/rules | 85 +++ error.gif | Bin 0 -> 560 bytes examples/counter/counter.sh | 25 + file.c | 402 ++++++++++++ gophermap | 36 ++ gophernicus.c | 735 ++++++++++++++++++++++ gophernicus.h | 410 ++++++++++++ gophernicus.xinetd | 11 + gophertag | 1 + install-sh | 520 +++++++++++++++ menu.c | 665 ++++++++++++++++++++ options.c | 186 ++++++ org.gophernicus.server.plist | 29 + platform.c | 277 ++++++++ session.c | 144 +++++ string.c | 416 ++++++++++++ 37 files changed, 5163 insertions(+) create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 README.Gophermap create mode 100644 TODO create mode 100755 bin2c.c create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/dirs create mode 100644 debian/docs create mode 100755 debian/gophernicus.config create mode 100644 debian/gophernicus.logcheck.ignore.server create mode 100644 debian/gophernicus.logrotate create mode 100644 debian/gophernicus.templates create mode 100644 debian/postinst create mode 100644 debian/postrm create mode 100644 debian/prerm create mode 100755 debian/rules create mode 100644 error.gif create mode 100755 examples/counter/counter.sh create mode 100644 file.c create mode 100644 gophermap create mode 100644 gophernicus.c create mode 100644 gophernicus.h create mode 100644 gophernicus.xinetd create mode 100644 gophertag create mode 100755 install-sh create mode 100644 menu.c create mode 100644 options.c create mode 100644 org.gophernicus.server.plist create mode 100644 platform.c create mode 100644 session.c create mode 100644 string.c 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 0000000000000000000000000000000000000000..c1704191a87054c35aa232ee19f3fe3c0af0bc29 GIT binary patch literal 560 zcmV-00?++NNk%w1VK4wN0OJS%&j0}a0090aCjU@R|9gA?qoV)G%K!WM|NsC0|NsC0 z|NsC0|NsC0|NsC0|NsC0A^8LW3IGTIEC2ui05AYB000F35Xng^1vqQVyRTX(0$=Hc zLLdl3y9S{tBic$Bc%B!;AZ#;E?RL-^3rM677?fmKhN5wotQMA-CJi$vNT&v?l|cSw zXPCeMOcf)^a6;-_!U%3)cQwu*z(^lycvE^je0@HDCtQSuhjA^DSb!FG1v-U>kCL2z zbQb^tmpE==nvXo4k#m$2jHY5y29I#EJ)A47AfcCfQ3pkb14O_ElDWBxt^i$P1ed_k zv%@{a(O1aG%3F8N(9zc^Dqi5g*vSFu%9eX%F@CJ>i6r+NsxE&(a-+O;lr0j z(5W8k2n1YspuoU^8UhHzr;uSehYty6kO=8vMT_1IGeX?hG2_RD7)9P2LdK8Elkifi zR5zr>Oi>K&-Aq@*#Y}nub>8ghQY6rYLRI$s2@+h=j1k3UJPPvZ$So?VnuF<7DWEha zQKsWstmsv&83j<4Nw$F0vuGcxy*Lo2+lQW7fld2P=Dm-1@v+FeOXXa@6vMnt$3o60 zC!q{m#g~Y3%Wa@`YF5{{%;RN|H_zT&s>A{M%mMy}eGP%BJ#0c)sy*oD%0RTIsI2LdK literal 0 HcmV?d00001 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