Merge pull request #16 from grufwub/development

Development pull v1.0-beta
master
Kim 4 years ago committed by GitHub
commit 9b18a9caeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,74 +14,141 @@ WARNING: the development branch is filled with lava, fear and capitalism.
# Features # Features
- Built with concurrency and efficiency in mind. - Built with security, concurrency and efficiency in mind.
- ZERO external dependencies. - ZERO external dependencies.
- Security focused -- chroots into server direrctory and drops - LRU file caching with user-controlled cache size, max cached file size
privileges. `maybe wait until stable release before use outside of hobby
setups.`
- LRU file caching -- with user-controlled cache size, max cached file size
and cache refresh frequency. and cache refresh frequency.
- Insert files within gophermaps, including automating reflowing of lines - CGI/1.1 support (see below for CGI environment variables set).
longer than (user definable) page width.
- Automatic replacement of `$hostname` or `$port` with the information of - URL encoding with query support.
the host the client is connecting to.
- User supplied footer text appended to gophermaps and directory listings. - Serve `DIR/gophermap` by default, else falls back to directory listing.
- Item type characters beyond RFC 1436 standard (see below). - Parsing of any files named `gophermap` or ending in `.gophermap` as
gophermaps.
- Separate system and access logging with output to file if requested (or to - Executable gophermap support.
disable both).
# Usage - Insert files with automated line reflowing, output of any CGI scripts
or executable gophermaps WITHIN gophermaps.
``` - Support for all commonly accepted item type characters (beyond just
gophor [args] RFC1436 support).
-root Change server root directory.
- Automatic replacement of `$hostname` or `$port` in gophermap lines with
current host information.
- User supplied footer text appended to gophermaps and directory listings.
-port Change server NON-TLS listening port. - File remapping support via regex, of format:
-hostname Change server hostname (FQDN, used to craft dir `/virtual/file -> /actual/file`
lists).
-bind-addr Change server bind-address (used in creating e.g. scripts within `cgi-bin` to the root directory:
socket). `/(?P<script>[^/]+) -> /cgi-bin/$script`
-user Drop to supplied user's UID and GID permissions Entries are parsed, compiled, and so matched-against in order.
before execution.
-system-log Path to gophor system log file, else use stderr. - Separate system and access logging with output and formatting options.
-access-log Path to gophor access log file, else use stderr. ## Please note
-cache-check Change file-cache freshness check frequency. ### Gophermap parsing
-cache-size Change max no. files in file-cache. Due to the way that gophermap parsing is handled, if a gophermap is larger than
the max cache'd file size or file caching is disabled (same as
same as setting max size to 0), these gophermaps WILL NOT be parsed by the server.
The features you will miss out on for these files are features listed
`[SERVER ONLY]` in the gophermap item types section below.
-cache-file-max Change maximum allowed size of a cached file. ### Chroots and privilege dropping
-page-width Change page width used when formatting output. Previously, chrooting to server directory and dropping privileges was supported
by using Go C bindings. This is not officially supported due to weird behaviour
with `.Set{U,G}id()` under Linux. As such, the feature has been dropped for
now.
-footer Change gophermap footer text (Unix new-line There is a near 10 year ongoing tracked issue
separated lines). (https://github.com/golang/go/issues/1435), and as soon as this patch gets
merged I'll add support: https://go-review.googlesource.com/c/go/+/210639
-no-footer-separator Disable footer text line separator. In place of removing this, request sanitization has been majorly improved and
checks are in place to prevent running Gophor as root.
-restrict-files New-line separated list of regex statements If you run into issues binding to a lower port number due to insufficient
restricting files from showing in directory listing. permissions then there are a few alternatives:
-description Change server description in generated caps.txt. - set gophor process capabilities: e.g.
`setcap 'cap_net_bind_service=+ep' /usr/local/bin/gophor`
-admin-email Change admin email in generated caps.txt. - use Docker (or some other solution) and configure port forwarding on the
host
-geoloc Change geolocation in generated caps.txt. - start gopher in it's own namespace in a chroot
-version Print version string. # Usage
```
gophor [args]
-root Change server root directory.
-bind-addr Change server bind-address (used in creating
socket).
-port Change server bind port.
-fwd-port Change port used in $port replacement strings
(e.g. when port forwarding).
-hostname Change server hostname (FQDN).
-system-log Path to gophor system log file.
-access-log Path to gophor access log file.
-log-output Change log output type (disable|stderr|file)
-log-opts Comma-separated list of lop opts (timestamp|ip)
-file-monitor-freq Change file-cache freshness check frequency.
-file-remap New-line separated list of file remappings of format:
/virtual/relative/path -> /actual/relative/path
-cache-size Change max no. files in file-cache.
-cache-file-max Change maximum allowed size of a cached file.
-disable-cache Disable file caching.
-page-width Change page width used when formatting output.
-footer Change gophermap footer text (Unix new-line
separated lines).
-no-footer-separator Disable footer text line separator.
-restrict-files New-line separated list of regex statements
(checked against absolute paths) restricting
file access.
-description Change server description in generated caps.txt.
-admin-email Change admin email in generated caps.txt.
-geoloc Change geolocation in generated caps.txt.
-disable-cgi Disable CGI and all executable support.
-http-compat-cgi Enable HTTP CGI script compatibility (will strip
HTTP headers).
-http-header-buf Change max CGI read count to look for and strip
HTTP headers before sending raw (bytes).
-safe-path Set safe PATH variable to be used when executing
CGI scripts, gophermaps and inline shell
commands.
-max-exec-time Change max executable CGI, gophermap and inline
shell command runtime.
-socket-write-buf Change socket write buffer size (bytes).
-socket-read-buf Change socket read buffer size (bytes).
-socket-read-max Change socket read count max (integer multiplier
to socket-read-buf-max).
-file-read-buf Change file read buffer size (bytes).
-socket-read-timeout Change socket read deadline (timeout).
-socket-write-timeout Change socket write deadline (timeout).
-version Print version string.
``` ```
# Supported gophermap item types # Supported gophermap item types
@ -100,6 +167,7 @@ text lines before sending to connecting clients.
``` ```
RFC 1436 Standard: RFC 1436 Standard:
Type | Treat as | Meaning Type | Treat as | Meaning
--------------------------
0 | TEXT | Regular file (text) 0 | TEXT | Regular file (text)
1 | MENU | Directory (menu) 1 | MENU | Directory (menu)
2 | EXTERNAL | CCSO flat db; other db 2 | EXTERNAL | CCSO flat db; other db
@ -117,6 +185,7 @@ Type | Treat as | Meaning
GopherII Standard: GopherII Standard:
Type | Treat as | Meaning Type | Treat as | Meaning
--------------------------
c | BINARY | Calendar file c | BINARY | Calendar file
d | BINARY | Word-processing document; PDF document d | BINARY | Word-processing document; PDF document
h | TEXT | HTML document h | TEXT | HTML document
@ -129,22 +198,28 @@ Type | Treat as | Meaning
Commonly used: Commonly used:
Type | Treat as | Meaning Type | Treat as | Meaning
--------------------------
. | - | Last line -- stop processing gophermap default
! | - | [SERVER ONLY] Menu title (set title ONCE per gophermap) ! | - | [SERVER ONLY] Menu title (set title ONCE per gophermap)
# | - | [SERVER ONLY] Comment, rest of line is ignored # | - | [SERVER ONLY] Comment, rest of line is ignored
- | - | [SERVER ONLY] Hide file/directory from directory listing - | - | [SERVER ONLY] Hide file/directory from directory listing
. | - | [SERVER ONLY] Last line -- stop processing gophermap default
* | - | [SERVER ONLY] Last line + directory listing -- stop processing * | - | [SERVER ONLY] Last line + directory listing -- stop processing
| | gophermap and end on a directory listing | | gophermap and end on a directory listing
= | - | [SERVER ONLY] Include subgophermap / regular file here. Prints = | - | [SERVER ONLY] Include or execute subgophermap, cgi-bin or regular
| | and formats file / gophermap in-place | | file here.
Planned to be supported:
Type | Treat as | Meaning
$ | - | [SERVER ONLY] Execute shell command and print stdout here
``` ```
# Encoding
By default, URLs are parsed as having standard (HTTP) URL encoding. All other
parsed text content (gophermaps) are treated as UTF-8, as this is the default
encoding scheme for Go strings. Support for more encoding schemes is planned
for the future.
# Compliance # Compliance
We aim to comply more with GopherII (see in references below).
## Item types ## Item types
Supported item types are listed above. Supported item types are listed above.
@ -156,6 +231,56 @@ Titles are sent as `i<title text>\tTITLE\tnull.host\t0`.
Web address links are sent as `h<text here>\tURL:<address>\thostname\tport`. Web address links are sent as `h<text here>\tURL:<address>\thostname\tport`.
An HTML redirect is sent in response to any requests beginning with `URL:`. An HTML redirect is sent in response to any requests beginning with `URL:`.
## CGI/1.1
The list of environment variables that gophor sets are as follows.
RFC 3875 standard:
```
# Set
GATEWAY INTERFACE
SERVER_SOFTWARE
SERVER_PROTOCOL
CONTENT_LENGTH
REQUEST_METHOD
SERVER_NAME
SERVER_PORT
REMOTE_ADDR
QUERY_STRING
SCRIPT_NAME
SCRIPT_FILENAME
# NOT set
Env Var | Reasoning
----------------------------------------------
PATH_INFO | This variable can fuck off, having to find the shortest
| valid part of path heirarchy in a URI every single
| CGI request so you can split and set this variable is SO
| inefficient. However, if someone more knowledgeable has
| other opinions or would like to point out where I'm wrong I
| will happily change my tune on this.
PATH_TRANSLATED | See above.
AUTH_TYPE | Until we implement authentication of some kind, ignoring.
CONTENT_TYPE | Very HTTP-centric relying on 'content-type' header.
REMOTE_IDENT | Remote client identity information.
REMOTE_HOST | Basically if the client has a resolving name (not just
| IP), not really necessary.
REMOTE_USER | Remote user id, not used as again no user auth yet.
```
Non-standard:
```
# Set
SELECTOR
DOCUMENT_ROOT
REQUEST_URI
PATH
COLUMNS
GOPHER_CHARSET
```
## Policy files ## Policy files
Upon request, `caps.txt` can be provided from the server root directory Upon request, `caps.txt` can be provided from the server root directory
@ -195,9 +320,9 @@ Possible Gophor errors:
## Terminating full stop ## Terminating full stop
Gophor will send a terminating full-stop for menus, but not for served Gophor will send a terminating full-stop for menus, but not for served
files. or executed files.
## Placeholder text ## Placeholder (null) text
All of the following are used as placeholder text in responses... All of the following are used as placeholder text in responses...
@ -209,59 +334,29 @@ Null port: `0`
# Todos # Todos
Shortterm: - Support setting character encoding
- Set default charset -- need to think about implementation here...
- Fix file cache only updating if main gophermap changes (but not sub files)
-- need to either rethink how we keep track of files, or rethink how
gophermaps are stored in memory.
Longterm:
- Finish inline shell scripting support -- current thinking is to either
perform a C fork very early on, or create a separate modules binary, and
either way the 2 processes interact via some IPC method. Could allow for
other modules too.
- Rotating logs -- have a check on start for a file-size, rotate out if the
file is too large. Possibly checks during run-time too?
- Add last-mod-time to directory listings -- have global time parser - improve organization of what logs go where (e.g. to sys or acc)
object, maybe separate out separate global instances of objects (e.g.
worker related, cache related, config related?)
- TLS support -- ~~requires a rethink of how we're passing port functions - Move filesystem_read functions to FileSystem struct function
generating gopher directory entries, also there is no definitive standard
for this yet~~ implemented these changes! figuring out gopher + TLS itself
though? no luck yet.
- Connection throttling + timeouts -- thread to keep track of list of - FastCGI support
recently connected IPs. Keep incremementing connection count and only
remove from list when `lastIncremented` time is greater than timeout
- More closely follow GoLang built-in net/http code style for worker -- just - Personal user gopherspaces
a neatness thing, maybe bring some performance improvements too and a
generally different way of approaching some of the solutions to problems we
have
# Please note - Rotating logs
During the initial writing phase the quality of git commit messages may be - TLS support
low and many changes are likely to be bundled together at a time, just
because the pace of development right now is rather break-neck.
As soon as we reach a stable point in development, or if other people start - Connection throttling + timeouts
contributing issues or PRs, whichever comes first, this will be changed
right away.
# Resources used # Resources used
Gopher-II (The Next Generation Gopher WWIS): Gopher-II (The Next Generation Gopher WWIS):
https://tools.ietf.org/html/draft-matavka-gopher-ii-00 https://tools.ietf.org/html/draft-matavka-gopher-ii-00
Gophernicus supported item types: Gophernicus source (a great gopher daemon in C):
https://github.com/gophernicus/gophernicus/blob/master/README.gophermap https://github.com/gophernicus/gophernicus
All of the below can be viewed from your standard web browser using All of the below can be viewed from your standard web browser using
floodgap's Gopher proxy: floodgap's Gopher proxy:

@ -3,13 +3,13 @@
set -e set -e
PROJECT='gophor' PROJECT='gophor'
VERSION="$(cat 'constants.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')" VERSION="$(cat 'gophor.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')"
GOVERSION="$(go version | sed -e 's|^go version go||' -e 's|\s.*$||')" GOVERSION="$(go version | sed -e 's|^go version go||' -e 's|\s.*$||')"
LOGFILE='build.log' LOGFILE='build.log'
OUTDIR="build-${VERSION}" OUTDIR="build-${VERSION}"
silent() { silent() {
"$@" > "$LOGFILE" 2>&1 "$@" >> "$LOGFILE" 2>&1
} }
build_for() { build_for() {
@ -22,15 +22,31 @@ build_for() {
echo "Building for ${os} ${archname}..." echo "Building for ${os} ${archname}..."
local filename="${OUTDIR}/${PROJECT}_${os}_${archname}" local filename="${OUTDIR}/${PROJECT}_${os}_${archname}"
CGO_ENABLED=1 CC="$toolchain" GOOS="$os" GOARCH="$arch" GOARM="$armversion" silent go build -trimpath -o "$filename" "$@" CGO_ENABLED=1 CC="$toolchain-gcc" GOOS="$os" GOARCH="$arch" GOARM="$armversion" silent go build -trimpath -o "$filename" "$@"
if [ "$?" -ne 0 ]; then if [ "$?" -ne 0 ]; then
echo "Failed!" echo "Failed!"
return 1 return 1
fi fi
echo "Compressing ${filename}..." echo "Attempting to compress ${filename}..."
silent upx --best "$filename"
silent upx -t "$filename" # First try compression with --best
cp "$filename" "${filename}.topack"
if (silent upx --best "${filename}.topack") && (silent upx -t "${filename}.topack"); then
echo "Succeeded with best compression levels!"
mv "${filename}.topack" "$filename"
else
# Failed! Before throwing in the towel, try regular compression levels
cp "$filename" "${filename}.topack"
if (silent upx "${filename}.topack") && (silent upx -t "${filename}.topack"); then
echo "Succeeded with regular compression levels!"
mv "${filename}.topack" "$filename"
else
echo "Failed!"
rm "${filename}.topack"
fi
fi
echo "" echo ""
} }
@ -39,33 +55,97 @@ echo "YOUR CC TOOLCHAIN LOCATIONS MAY DIFFER"
echo "IF THE SCRIPT FAILS, CHECK THE OUTPUT OF: ${LOGFILE}" echo "IF THE SCRIPT FAILS, CHECK THE OUTPUT OF: ${LOGFILE}"
echo "" echo ""
# Clean logfile
rm -f "$LOGFILE"
# Clean and recreate directory # Clean and recreate directory
rm -rf "$OUTDIR" rm -rf "$OUTDIR"
mkdir -p "$OUTDIR" mkdir -p "$OUTDIR"
# Build time :) # Build time :)
build_for '386' 'i686-linux-musl-gcc' 'linux' '386' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'amd64' 'x86_64-linux-musl-gcc' 'linux' 'amd64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' # Linux
build_for '386' 'i686-linux-musl' 'linux' '386' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'amd64' 'x86_64-linux-musl' 'linux' 'amd64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5' 'arm-linux-musleabi' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5hf' 'arm-linux-musleabihf' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6' 'arm-linux-musleabi' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6hf' 'arm-linux-musleabihf' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv7lhf' 'armv7l-linux-musleabihf' 'linux' 'arm' '7' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'arm64' 'aarch64-linux-musl' 'linux' 'arm64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mips' 'mips-linux-musl' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipshf' 'mips-linux-muslhf' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipsle' 'mipsel-linux-musl' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipslehf' 'mipsel-linux-muslhf' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'ppc64le' 'powerpc64le-linux-musl' 'linux' 'ppc64le' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
# Netbsd
build_for '386' 'i686-linux-musl' 'netbsd' '386' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'amd64' 'x86_64-linux-musl' 'netbsd' 'amd64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5' 'arm-linux-musleabi' 'netbsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5hf' 'arm-linux-musleabihf' 'netbsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6' 'arm-linux-musleabi' 'netbsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6hf' 'arm-linux-musleabihf' 'netbsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv7lhf' 'armv7l-linux-musleabihf' 'netbsd' 'arm' '7' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'arm64' 'aarch64-linux-musl' 'netbsd' 'arm64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
# Openbsd
build_for '386' 'i686-linux-musl' 'openbsd' '386' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'amd64' 'x86_64-linux-musl' 'openbsd' 'amd64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5' 'arm-linux-musleabi' 'openbsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5hf' 'arm-linux-musleabihf' 'openbsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6' 'arm-linux-musleabi' 'openbsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6hf' 'arm-linux-musleabihf' 'openbsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv7lhf' 'armv7l-linux-musleabihf' 'openbsd' 'arm' '7' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'arm64' 'aarch64-linux-musl' 'openbsd' 'arm64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5' 'arm-linux-musleabi-gcc' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' # Freebsd
build_for '386' 'i686-linux-musl' 'freebsd' '386' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv5hf' 'arm-linux-musleabihf-gcc' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'amd64' 'x86_64-linux-musl' 'freebsd' 'amd64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6' 'arm-linux-musleabi-gcc' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'armv5' 'arm-linux-musleabi' 'freebsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv6hf' 'arm-linux-musleabihf-gcc' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'armv5hf' 'arm-linux-musleabihf' 'freebsd' 'arm' '5' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'armv7lhf' 'armv7l-linux-musleabihf-gcc' 'linux' 'arm' '7' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'armv6' 'arm-linux-musleabi' 'freebsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'arm64' 'aarch64-linux-musl-gcc' 'linux' 'arm64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'armv6hf' 'arm-linux-musleabihf' 'freebsd' 'arm' '6' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mips' 'mips-linux-musl-gcc' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'armv7lhf' 'armv7l-linux-musleabihf' 'freebsd' 'arm' '7' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipshf' 'mips-linux-muslhf-gcc' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'arm64' 'aarch64-linux-musl' 'freebsd' 'arm64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipsle' 'mipsel-linux-musl-gcc' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' # Dragonfly
build_for 'amd64' 'x86_64-linux-musl' 'dragonfly' 'amd64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'mipslehf' 'mipsel-linux-muslhf-gcc' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' # Macos
build_for '386' 'i686-linux-musl' 'darwin' '386' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
build_for 'ppc64le' 'powerpc64le-linux-musl-gcc' 'linux' 'ppc64le' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' build_for 'amd64' 'x86_64-linux-musl' 'darwin' 'amd64' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'

@ -1,38 +0,0 @@
#!/bin/sh
set -e
PROJECT='gophor'
OUTDIR='build'
VERSION="$(cat 'constants.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')"
GOVERSION="$(go version | sed -e 's|^go version go||' -e 's|\s.*$||')"
LOGFILE='build.log'
silent() {
"$@" > "$LOGFILE" 2>&1
}
build_for() {
local archname="$1" toolchain="$2" os="$3" arch="$4"
shift 4
if [ "$arch" = 'arm' ]; then
local armversion="$1"
shift 1
fi
echo "Building for ${os} ${archname}..."
local filename="${OUTDIR}/${PROJECT}_${os}_${archname}"
CGO_ENABLED=1 CC="$toolchain" GOOS="$os" GOARCH="$arch" GOARM="$armversion" silent go build -trimpath -o "$filename" "$@"
if [ "$?" -ne 0 ]; then
echo "Failed!"
return 1
fi
echo "Compressing ${filename}..."
silent upx --best "$filename"
silent upx -t "$filename"
echo ""
}
# Build time :)
build_for 'amd64' 'x86_64-linux-musl-gcc' 'linux' 'amd64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'

@ -1,8 +1,8 @@
package main package main
import ( import (
"time"
"regexp" "regexp"
"log"
) )
/* ServerConfig: /* ServerConfig:
@ -12,38 +12,35 @@ import (
* and file cache) * and file cache)
*/ */
type ServerConfig struct { type ServerConfig struct {
/* Base settings */ /* Executable Settings */
RootDir string Env []string
CgiEnv []string
CgiEnabled bool
MaxExecRunTime time.Duration
/* Content settings */ /* Content settings */
FooterText []byte FooterText []byte
PageWidth int PageWidth int
RestrictedFiles []*regexp.Regexp
/* Logging */ /* Logging */
SystemLogger *log.Logger SysLog LoggerInterface
AccessLogger *log.Logger AccLog LoggerInterface
/* Filesystem access */ /* Filesystem access */
FileSystem *FileSystem FileSystem *FileSystem
}
/* Buffer sizes */
func (config *ServerConfig) LogSystem(fmt string, args ...interface{}) { SocketWriteBufSize int
config.SystemLogger.Printf(":: I :: "+fmt, args...) SocketReadBufSize int
} SocketReadMax int
SkipPrefixBufSize int
func (config *ServerConfig) LogSystemError(fmt string, args ...interface{}) { FileReadBufSize int
config.SystemLogger.Printf(":: E :: "+fmt, args...)
} /* Socket deadlines */
SocketReadDeadline time.Duration
func (config *ServerConfig) LogSystemFatal(fmt string, args ...interface{}) { SocketWriteDeadline time.Duration
config.SystemLogger.Fatalf(":: F :: "+fmt, args...)
} /* Precompiled regular expressions */
RgxGophermap *regexp.Regexp
func (config *ServerConfig) LogAccess(sourceAddr, fmt string, args ...interface{}) { RgxCgiBin *regexp.Regexp
config.AccessLogger.Printf(":: I :: ["+sourceAddr+"] "+fmt, args...)
}
func (config *ServerConfig) LogAccessError(sourceAddr, fmt string, args ...interface{}) {
config.AccessLogger.Printf(":: E :: ["+sourceAddr+"] "+fmt, args...)
} }

@ -1,27 +1,64 @@
package main package main
import ( import (
"io"
"net" "net"
"time"
"bufio"
"strconv"
) )
/* Data structure to hold specific host details */
type ConnHost struct { type ConnHost struct {
Name string /* Hold host specific details */
Port string name string
hostport string
fwdport string
}
func (host *ConnHost) Name() string {
return host.name
}
func (host *ConnHost) Port() string {
return host.fwdport
}
func (host *ConnHost) RealPort() string {
return host.hostport
}
type ConnClient struct {
/* Hold client specific details */
ip string
port string
}
func (client *ConnClient) Ip() string {
return client.ip
}
func (client *ConnClient) Port() string {
return client.port
}
func (client *ConnClient) AddrStr() string {
return client.Ip()+":"+client.Port()
} }
/* Simple wrapper to Listener that holds onto virtual
* host information and generates GophorConn
* instances on each accept
*/
type GophorListener struct { type GophorListener struct {
/* Simple net.Listener wrapper that holds onto virtual
* host information + generates Worker instances on Accept()
*/
Listener net.Listener Listener net.Listener
Host *ConnHost Host *ConnHost
Root string
} }
func BeginGophorListen(bindAddr, hostname, port string) (*GophorListener, error) { func BeginGophorListen(bindAddr, hostname, port, fwdPort, rootDir string) (*GophorListener, error) {
gophorListener := new(GophorListener) gophorListener := new(GophorListener)
gophorListener.Host = &ConnHost{ hostname, port } gophorListener.Host = &ConnHost{ hostname, port, fwdPort }
gophorListener.Root = rootDir
var err error var err error
gophorListener.Listener, err = net.Listen("tcp", bindAddr+":"+port) gophorListener.Listener, err = net.Listen("tcp", bindAddr+":"+port)
@ -32,42 +69,107 @@ func BeginGophorListen(bindAddr, hostname, port string) (*GophorListener, error)
} }
} }
func (l *GophorListener) Accept() (*GophorConn, error) { func (l *GophorListener) Accept() (*Worker, error) {
conn, err := l.Listener.Accept() conn, err := l.Listener.Accept()
if err != nil { if err != nil {
return nil, err return nil, err
} }
gophorConn := new(GophorConn) /* Should always be ok as listener is type TCP (see above) */
gophorConn.Conn = conn addr, _ := conn.RemoteAddr().(*net.TCPAddr)
gophorConn.Host = &ConnHost{ l.Host.Name, l.Host.Port } client := &ConnClient{ addr.IP.String(), strconv.Itoa(addr.Port) }
return gophorConn, nil
return &Worker{ NewBufferedDeadlineConn(conn), l.Host, client, l.Root }, nil
} }
func (l *GophorListener) Addr() net.Addr { type DeadlineConn struct {
return l.Listener.Addr() /* Simple wrapper to net.Conn that sets deadlines
* on each call to Read() / Write()
*/
conn net.Conn
} }
/* Simple wrapper to Conn with easier acccess func NewDeadlineConn(conn net.Conn) *DeadlineConn {
* to hostname / port information return &DeadlineConn{ conn }
*/ }
type GophorConn struct {
Conn net.Conn func (c *DeadlineConn) Read(b []byte) (int, error) {
Host *ConnHost /* Implements a regular net.Conn + updates deadline */
c.conn.SetReadDeadline(time.Now().Add(Config.SocketReadDeadline))
return c.conn.Read(b)
}
func (c *DeadlineConn) Write(b []byte) (int, error) {
/* Implements a regular net.Conn + updates deadline */
c.conn.SetWriteDeadline(time.Now().Add(Config.SocketWriteDeadline))
return c.conn.Write(b)
}
func (c *DeadlineConn) Close() error {
/* Close */
return c.conn.Close()
}
type BufferedDeadlineConn struct {
/* Wrapper around DeadlineConn that provides buffered
* reads and writes.
*/
conn *DeadlineConn
buffer *bufio.ReadWriter
}
func NewBufferedDeadlineConn(conn net.Conn) *BufferedDeadlineConn {
deadlineConn := NewDeadlineConn(conn)
return &BufferedDeadlineConn{
deadlineConn,
bufio.NewReadWriter(
bufio.NewReaderSize(deadlineConn, Config.SocketReadBufSize),
bufio.NewWriterSize(deadlineConn, Config.SocketWriteBufSize),
),
}
}
func (c *BufferedDeadlineConn) ReadLine() ([]byte, error) {
/* Return slice */
b := make([]byte, 0)
for len(b) < Config.SocketReadMax {
/* Read line */
line, isPrefix, err := c.buffer.ReadLine()
if err != nil {
return nil, err
}
/* Add to return slice */
b = append(b, line...)
/* If !isPrefix, we can break-out */
if !isPrefix {
break
}
}
return b, nil
} }
func (c *GophorConn) Read(b []byte) (int, error) { func (c *BufferedDeadlineConn) Write(b []byte) (int, error) {
return c.Conn.Read(b) return c.buffer.Write(b)
} }
func (c *GophorConn) Write(b []byte) (int, error) { func (c *BufferedDeadlineConn) WriteData(b []byte) error {
return c.Conn.Write(b) _, err := c.buffer.Write(b)
return err
} }
func (c *GophorConn) RemoteAddr() net.Addr { func (c *BufferedDeadlineConn) WriteRaw(r io.Reader) error {
return c.Conn.RemoteAddr() _, err := c.buffer.ReadFrom(r)
return err
} }
func (c *GophorConn) Close() error { func (c *BufferedDeadlineConn) Close() error {
return c.Conn.Close() /* First flush buffer, then close */
c.buffer.Flush()
return c.conn.Close()
} }

@ -1,97 +0,0 @@
package main
const (
/* Gophor */
GophorVersion = "0.5-alpha"
/* Socket settings */
SocketReadBufSize = 256 /* Supplied selector shouldn't be longer than this anyways */
MaxSocketReadChunks = 1
FileReadBufSize = 1024
/* Parsing */
DOSLineEnd = "\r\n"
UnixLineEnd = "\n"
End = "."
Tab = "\t"
LastLine = End+DOSLineEnd
/* Line creation */
MaxUserNameLen = 70 /* RFC 1436 standard */
MaxSelectorLen = 255 /* RFC 1436 standard */
NullSelector = "-"
NullHost = "null.host"
NullPort = "0"
SelectorErrorStr = "selector_length_error"
GophermapRenderErrorStr = ""
/* Replacement strings */
ReplaceStrHostname = "$hostname"
ReplaceStrPort = "$port"
/* Filesystem */
GophermapFileStr = "gophermap"
CapsTxtStr = "caps.txt"
RobotsTxtStr = "robots.txt"
/* Misc */
BytesInMegaByte = 1048576.0
)
/*
* Item type characters:
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
* and Gophernicus project. Those with ALL-CAPS descriptions in
* [square brackets] defined and used by Gophernicus, a popular
* Gopher server.
*/
type ItemType byte
const (
/* RFC 1436 Standard */
TypeFile = ItemType('0') /* Regular file (text) */
TypeDirectory = ItemType('1') /* Directory (menu) */
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
TypeError = ItemType('3') /* Error message */
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
TypeSearch = ItemType('7') /* Query search engine or CGI script */
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
TypeGif = ItemType('g') /* GIF format image file (just use I) */
TypeImage = ItemType('I') /* Any format image file */
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
/* GopherII Standard */
TypeCalendar = ItemType('c') /* Calendar file */
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
TypeHtml = ItemType('h') /* HTML document */
TypeInfo = ItemType('i') /* Informational text (not selectable) */
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
TypeMail = ItemType('M') /* Email repository (MBOX) */
TypeAudio = ItemType('s') /* Audio recordings */
TypeXml = ItemType('x') /* eXtensible Markup Language document */
TypeVideo = ItemType(';') /* Video files */
/* Commonly Used */
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on a directory listing */
/* Planned To Be Supported */
TypeExec = ItemType('$') /* [SERVER ONLY] Execute shell command and print stdout here */
/* Default type */
TypeDefault = TypeBin
/* Gophor specific types */
TypeInfoNotStated = ItemType('z') /* [INTERNAL USE] */
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
)

@ -9,23 +9,41 @@ type ErrorCode int
type ErrorResponseCode int type ErrorResponseCode int
const ( const (
/* Filesystem */ /* Filesystem */
PathEnumerationErr ErrorCode = iota PathEnumerationErr ErrorCode = iota
IllegalPathErr ErrorCode = iota IllegalPathErr ErrorCode = iota
FileStatErr ErrorCode = iota FileStatErr ErrorCode = iota
FileOpenErr ErrorCode = iota FileOpenErr ErrorCode = iota
FileReadErr ErrorCode = iota FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota DirListErr ErrorCode = iota
/* Sockets */ /* Sockets */
SocketWriteErr ErrorCode = iota SocketWriteErr ErrorCode = iota
SocketWriteCountErr ErrorCode = iota SocketWriteRawErr ErrorCode = iota
/* Parsing */ /* Parsing */
InvalidRequestErr ErrorCode = iota InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr ErrorCode = iota EmptyItemTypeErr ErrorCode = iota
EntityPortParseErr ErrorCode = iota InvalidGophermapErr ErrorCode = iota
InvalidGophermapErr ErrorCode = iota
/* Executing */
CommandStartErr ErrorCode = iota
CommandExitCodeErr ErrorCode = iota
CgiOutputErr ErrorCode = iota
CgiDisabledErr ErrorCode = iota
RestrictedCommandErr ErrorCode = iota
/* Wrapping CGI http status codes */
CgiStatus400Err ErrorCode = iota
CgiStatus401Err ErrorCode = iota
CgiStatus403Err ErrorCode = iota
CgiStatus404Err ErrorCode = iota
CgiStatus408Err ErrorCode = iota
CgiStatus410Err ErrorCode = iota
CgiStatus500Err ErrorCode = iota
CgiStatus501Err ErrorCode = iota
CgiStatus503Err ErrorCode = iota
CgiStatusUnknownErr ErrorCode = iota
/* Error Response Codes */ /* Error Response Codes */
ErrorResponse200 ErrorResponseCode = iota ErrorResponse200 ErrorResponseCode = iota
@ -67,19 +85,47 @@ func (e *GophorError) Error() string {
str = "directory read fail" str = "directory read fail"
case SocketWriteErr: case SocketWriteErr:
str = "socket write fail" str = "socket write error"
case SocketWriteCountErr: case SocketWriteRawErr:
str = "socket write count mismatch" str = "socket write readFrom error"
case InvalidRequestErr: case InvalidRequestErr:
str = "invalid request data" str = "invalid request data"
case EmptyItemTypeErr:
str = "line string provides no dir entity type"
case EntityPortParseErr:
str = "parsing dir entity port"
case InvalidGophermapErr: case InvalidGophermapErr:
str = "invalid gophermap" str = "invalid gophermap"
case CommandStartErr:
str = "command start fail"
case CgiOutputErr:
str = "cgi output format error"
case CommandExitCodeErr:
str = "command exit code non-zero"
case CgiDisabledErr:
str = "ignoring /cgi-bin request, CGI disabled"
case RestrictedCommandErr:
str = "command use restricted"
case CgiStatus400Err:
str = "CGI script error status 400"
case CgiStatus401Err:
str = "CGI script error status 401"
case CgiStatus403Err:
str = "CGI script error status 403"
case CgiStatus404Err:
str = "CGI script error status 404"
case CgiStatus408Err:
str = "CGI script error status 408"
case CgiStatus410Err:
str = "CGI script error status 410"
case CgiStatus500Err:
str = "CGI script error status 500"
case CgiStatus501Err:
str = "CGI script error status 501"
case CgiStatus503Err:
str = "CGI script error status 503"
case CgiStatusUnknownErr:
str = "CGI script error unknown status code"
default: default:
str = "Unknown" str = "Unknown"
} }
@ -113,16 +159,44 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
/* These are errors _while_ sending, no point trying to send error */ /* These are errors _while_ sending, no point trying to send error */
case SocketWriteErr: case SocketWriteErr:
return NoResponse return NoResponse
case SocketWriteCountErr: case SocketWriteRawErr:
return NoResponse return NoResponse
case InvalidRequestErr: case InvalidRequestErr:
return ErrorResponse400 return ErrorResponse400
case EmptyItemTypeErr: case InvalidGophermapErr:
return ErrorResponse500 return ErrorResponse500
case EntityPortParseErr:
case CommandStartErr:
return ErrorResponse500 return ErrorResponse500
case InvalidGophermapErr: case CommandExitCodeErr:
return ErrorResponse500
case CgiOutputErr:
return ErrorResponse500
case CgiDisabledErr:
return ErrorResponse404
case RestrictedCommandErr:
return ErrorResponse500
case CgiStatus400Err:
return ErrorResponse400
case CgiStatus401Err:
return ErrorResponse401
case CgiStatus403Err:
return ErrorResponse403
case CgiStatus404Err:
return ErrorResponse404
case CgiStatus408Err:
return ErrorResponse408
case CgiStatus410Err:
return ErrorResponse410
case CgiStatus500Err:
return ErrorResponse500
case CgiStatus501Err:
return ErrorResponse501
case CgiStatus503Err:
return ErrorResponse503
case CgiStatusUnknownErr:
return ErrorResponse500 return ErrorResponse500
default: default:
@ -141,14 +215,16 @@ func generateGopherErrorResponseFromCode(code ErrorCode) []byte {
/* Generates gopher protocol compatible error response for response code */ /* Generates gopher protocol compatible error response for response code */
func generateGopherErrorResponse(code ErrorResponseCode) []byte { func generateGopherErrorResponse(code ErrorResponseCode) []byte {
return buildError(code.String()) return buildErrorLine(code.String())
} }
/* Error response code to string */ /* Error response code to string */
func (e ErrorResponseCode) String() string { func (e ErrorResponseCode) String() string {
switch e { switch e {
case ErrorResponse200: case ErrorResponse200:
return "200 OK" /* Should not have reached here */
Config.SysLog.Fatal("", "Passed error response 200 to error handler, SHOULD NOT HAVE DONE THIS\n")
return ""
case ErrorResponse400: case ErrorResponse400:
return "400 Bad Request" return "400 Bad Request"
case ErrorResponse401: case ErrorResponse401:
@ -169,7 +245,7 @@ func (e ErrorResponseCode) String() string {
return "503 Service Unavailable" return "503 Service Unavailable"
default: default:
/* Should not have reached here */ /* Should not have reached here */
Config.LogSystemFatal("Unhandled ErrorResponseCode type\n") Config.SysLog.Fatal("", "Unhandled ErrorResponseCode type\n")
return "" return ""
} }
} }

@ -0,0 +1,160 @@
package main
import (
"io"
"os/exec"
"syscall"
"strconv"
"time"
)
/* Setup initial (i.e. constant) gophermap / command environment variables */
func setupExecEnviron(path string) []string {
return []string {
envKeyValue("PATH", path),
}
}
/* Setup initial (i.e. constant) CGI environment variables */
func setupInitialCgiEnviron(path, charset string) []string {
return []string{
/* RFC 3875 standard */
envKeyValue("GATEWAY_INTERFACE", "CGI/1.1"), /* MUST be set to the dialect of CGI being used by the server */
envKeyValue("SERVER_SOFTWARE", "gophor/"+GophorVersion), /* MUST be set to name and version of server software serving this request */
envKeyValue("SERVER_PROTOCOL", "gopher"), /* MUST be set to name and version of application protocol used for this request */
envKeyValue("CONTENT_LENGTH", "0"), /* Contains size of message-body attached (always 0 so we set here) */
envKeyValue("REQUEST_METHOD", "GET"), /* MUST be set to method by which script should process request. Always GET */
/* Non-standard */
envKeyValue("PATH", path),
envKeyValue("COLUMNS", strconv.Itoa(Config.PageWidth)),
envKeyValue("GOPHER_CHARSET", charset),
}
}
/* Generate CGI environment */
func generateCgiEnvironment(responder *Responder) []string {
/* Get initial CgiEnv variables */
env := Config.CgiEnv
env = append(env, envKeyValue("SERVER_NAME", responder.Host.Name())) /* MUST be set to name of server host client is connecting to */
env = append(env, envKeyValue("SERVER_PORT", responder.Host.Port())) /* MUST be set to the server port that client is connecting to */
env = append(env, envKeyValue("REMOTE_ADDR", responder.Client.Ip())) /* Remote client addr, MUST be set */
env = append(env, envKeyValue("QUERY_STRING", responder.Request.Parameters)) /* URL encoded search or parameter string, MUST be set even if empty */
env = append(env, envKeyValue("SCRIPT_NAME", "/"+responder.Request.Path.Relative())) /* URI path (not URL encoded) which could identify the CGI script (rather than script's output) */
env = append(env, envKeyValue("SCRIPT_FILENAME", responder.Request.Path.Absolute())) /* Basically SCRIPT_NAME absolute path */
env = append(env, envKeyValue("SELECTOR", responder.Request.Path.Selector()))
env = append(env, envKeyValue("DOCUMENT_ROOT", responder.Request.Path.RootDir()))
env = append(env, envKeyValue("REQUEST_URI", "/"+responder.Request.Path.Relative()+responder.Request.Parameters))
return env
}
/* Execute a CGI script (pointer to correct function) */
var executeCgi func(*Responder) *GophorError
/* Execute CGI script and serve as-is */
func executeCgiNoHttp(responder *Responder) *GophorError {
return execute(responder.Conn, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
}
/* Execute CGI script and strip HTTP headers */
func executeCgiStripHttp(responder *Responder) *GophorError {
/* HTTP header stripping writer that also parses HTTP status codes */
httpStripWriter := NewHttpStripWriter(responder.Conn)
/* Execute the CGI script using the new httpStripWriter */
gophorErr := execute(httpStripWriter, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
/* httpStripWriter's error takes priority as it might have parsed the status code */
cgiStatusErr := httpStripWriter.FinishUp()
if cgiStatusErr != nil {
return cgiStatusErr
} else {
return gophorErr
}
}
/* Execute any file (though only allowed are gophermaps) */
func executeFile(responder *Responder) *GophorError {
return execute(responder.Conn, Config.Env, responder.Request.Path.Absolute())
}
/* Execute a supplied path with arguments and environment, to writer */
func execute(writer io.Writer, env []string, path string) *GophorError {
/* If CGI disbabled, just return error */
if !Config.CgiEnabled {
return &GophorError{ CgiDisabledErr, nil }
}
/* Setup command */
cmd := exec.Command(path)
/* Set new proccess group id */
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
/* Setup cmd env */
cmd.Env = env
/* Setup out buffer */
cmd.Stdout = writer
/* Start executing! */
err := cmd.Start()
if err != nil {
return &GophorError{ CommandStartErr, err }
}
/* Setup timer goroutine to kill cmd after x time */
go func() {
time.Sleep(Config.MaxExecRunTime)
if cmd.ProcessState != nil {
/* We've already finished */
return
}
/* Get process group id */
pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err != nil {
Config.SysLog.Fatal("", "Process unfinished, PGID not found!\n")
}
/* Kill process group! */
err = syscall.Kill(-pgid, syscall.SIGTERM)
if err != nil {
Config.SysLog.Fatal("", "Error stopping process group %d: %s\n", pgid, err.Error())
}
}()
/* Wait for command to finish, get exit code */
err = cmd.Wait()
exitCode := 0
if err != nil {
/* Error, try to get exit code */
exitError, ok := err.(*exec.ExitError)
if ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
} else {
exitCode = 1
}
} else {
/* No error! Get exit code direct from command */
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
}
if exitCode != 0 {
/* If non-zero exit code return error */
Config.SysLog.Error("", "Error executing: %s\n", path)
return &GophorError{ CommandExitCodeErr, err }
} else {
return nil
}
}
/* Just neatens creating an environment KEY=VALUE string */
func envKeyValue(key, value string) string {
return key+"="+value
}

@ -3,19 +3,27 @@ package main
import ( import (
"bytes" "bytes"
"bufio" "bufio"
"strings" "os"
) )
/* GeneratedFileContents: type FileContents interface {
* The simplest implementation of FileContents that /* Interface that provides an adaptable implementation
* stores some bytes and does nothing else. * for holding onto some level of information about the
*/ * contents of a file.
*/
Render(*Responder) *GophorError
Load() *GophorError
Clear()
}
type GeneratedFileContents struct { type GeneratedFileContents struct {
contents []byte /* Super simple, holds onto a slice of bytes */
Contents []byte
} }
func (fc *GeneratedFileContents) Render(request *FileSystemRequest) []byte { func (fc *GeneratedFileContents) Render(responder *Responder) *GophorError {
return fc.contents return responder.WriteData(fc.Contents)
} }
func (fc *GeneratedFileContents) Load() *GophorError { func (fc *GeneratedFileContents) Load() *GophorError {
@ -27,141 +35,174 @@ func (fc *GeneratedFileContents) Clear() {
/* do nothing */ /* do nothing */
} }
/* RegularFileContents:
* Very simple implementation of FileContents that just
* buffered reads from the stored file path, stores the
* read bytes in a slice and returns when requested.
*/
type RegularFileContents struct { type RegularFileContents struct {
path string /* Simple implemention that holds onto a RequestPath
contents []byte * and slice containing cache'd content
*/
Path *RequestPath
Contents []byte
} }
func (fc *RegularFileContents) Render(request *FileSystemRequest) []byte { func (fc *RegularFileContents) Render(responder *Responder) *GophorError {
/* Here we can ignore the extra data in request. return responder.WriteData(fc.Contents)
* We are but a simple cache'd file
*/
return fc.contents
} }
func (fc *RegularFileContents) Load() *GophorError { func (fc *RegularFileContents) Load() *GophorError {
/* Load the file into memory */ /* Load the file into memory */
var gophorErr *GophorError var gophorErr *GophorError
fc.contents, gophorErr = bufferedRead(fc.path) fc.Contents, gophorErr = bufferedRead(fc.Path.Absolute())
return gophorErr return gophorErr
} }
func (fc *RegularFileContents) Clear() { func (fc *RegularFileContents) Clear() {
fc.contents = nil fc.Contents = nil
} }
/* GophermapContents:
* Implementation of FileContents that reads and
* parses a gophermap file into a slice of gophermap
* sections, then renders and returns these sections
* when requested.
*/
type GophermapContents struct { type GophermapContents struct {
path string /* Holds onto a RequestPath and slice containing individually
sections []GophermapSection * renderable sections of the gophermap.
} */
func (gc *GophermapContents) Render(request *FileSystemRequest) []byte { Request *Request
returnContents := make([]byte, 0) Sections []GophermapSection
}
/* We don't just want to read the contents, each section func (gc *GophermapContents) Render(responder *Responder) *GophorError {
* in the sections slice needs a call to render() to /* Render and send each of the gophermap sections */
* perform their own required actions in producing a var gophorErr *GophorError
* sendable byte slice. for _, line := range gc.Sections {
*/ gophorErr = line.Render(responder)
for _, line := range gc.sections {
content, gophorErr := line.Render(request)
if gophorErr != nil { if gophorErr != nil {
content = buildInfoLine(GophermapRenderErrorStr) Config.SysLog.Error("", "Error executing gophermap contents: %s\n", gophorErr.Error())
return &GophorError{ InvalidGophermapErr, gophorErr }
} }
returnContents = append(returnContents, content...)
} }
/* The footer added later contains last line, don't need to worry */ /* End on footer text (including lastline) */
return responder.WriteData(Config.FooterText)
return returnContents
} }
func (gc *GophermapContents) Load() *GophorError { func (gc *GophermapContents) Load() *GophorError {
/* Load the gophermap into memory as gophermap sections */ /* Load the gophermap into memory as gophermap sections */
var gophorErr *GophorError var gophorErr *GophorError
gc.sections, gophorErr = readGophermap(gc.path) gc.Sections, gophorErr = readGophermap(gc.Request)
return gophorErr if gophorErr != nil {
return &GophorError{ InvalidGophermapErr, gophorErr }
} else {
return nil
}
} }
func (gc *GophermapContents) Clear() { func (gc *GophermapContents) Clear() {
gc.sections = nil gc.Sections = nil
} }
/* GophermapSection:
* Provides an interface for different stored sections
* of a gophermap file, whether it's static text that we
* may want stored as-is, or the data required for a dir
* listing or command executed that we may want updated
* upon each file cache request.
*/
type GophermapSection interface { type GophermapSection interface {
Render(*FileSystemRequest) ([]byte, *GophorError) /* Interface for storing differring types of gophermap
* sections to render when necessary
*/
Render(*Responder) *GophorError
} }
/* GophermapText: type GophermapTextSection struct {
* Simple implementation of GophermapSection that holds
* onto a static section of text as a slice of bytes.
*/
type GophermapText struct {
Contents []byte Contents []byte
} }
func NewGophermapText(contents []byte) *GophermapText { func (s *GophermapTextSection) Render(responder *Responder) *GophorError {
return &GophermapText{ contents } return responder.WriteData(replaceStrings(string(s.Contents), responder.Host))
} }
func (s *GophermapText) Render(request *FileSystemRequest) ([]byte, *GophorError) { type GophermapDirectorySection struct {
return replaceStrings(string(s.Contents), request.Host), nil /* Holds onto a directory path, and a list of files
* to hide from the client when rendering.
*/
Request *Request
Hidden map[string]bool
} }
/* GophermapDirListing: func (g *GophermapDirectorySection) Render(responder *Responder) *GophorError {
* An implementation of GophermapSection that holds onto a /* Create new responder from supplied and using stored path */
* path and a requested list of hidden files, then enumerates return listDir(responder.CloneWithRequest(g.Request), g.Hidden)
* the supplied paths (ignoring hidden files) when the content
* Render() call is received.
*/
type GophermapDirListing struct {
Path string
Hidden map[string]bool
} }
func NewGophermapDirListing(path string) *GophermapDirListing { type GophermapFileSection struct {
return &GophermapDirListing{ path, nil } /* Holds onto a file path to be read and rendered when requested */
Request *Request
} }
func (s *GophermapDirListing) Render(request *FileSystemRequest) ([]byte, *GophorError) { func (g *GophermapFileSection) Render(responder *Responder) *GophorError {
/* We could just pass the request directly, but in case the request fileContents, gophorErr := readIntoGophermap(g.Request.Path.Absolute())
* path happens to differ for whatever reason we create a new one if gophorErr != nil {
*/ return gophorErr
return listDir(&FileSystemRequest{ s.Path, request.Host }, s.Hidden) }
return responder.WriteData(fileContents)
}
type GophermapSubmapSection struct {
/* Holds onto a gophermap path to be read and rendered when requested */
Request *Request
}
func (g *GophermapSubmapSection) Render(responder *Responder) *GophorError {
/* Load the gophermap into memory as gophermap sections */
sections, gophorErr := readGophermap(g.Request)
if gophorErr != nil {
return gophorErr
}
/* Render and send each of the gophermap sections */
for _, line := range sections {
gophorErr = line.Render(responder)
if gophorErr != nil {
Config.SysLog.Error("", "Error executing gophermap contents: %s\n", gophorErr.Error())
}
}
return nil
}
type GophermapExecCgiSection struct {
/* Holds onto a request with CGI script path and supplied parameters */
Request *Request
} }
func readGophermap(path string) ([]GophermapSection, *GophorError) { func (g *GophermapExecCgiSection) Render(responder *Responder) *GophorError {
/* Create new filesystem request from mixture of stored + supplied */
return executeCgi(responder.CloneWithRequest(g.Request))
}
type GophermapExecFileSection struct {
/* Holds onto a request with executable file path and supplied arguments */
Request *Request
}
func (g *GophermapExecFileSection) Render(responder *Responder) *GophorError {
/* Create new responder from supplied and using stored path */
return executeFile(responder.CloneWithRequest(g.Request))
}
/* Read and parse a gophermap into separately cacheable and renderable GophermapSection */
func readGophermap(request *Request) ([]GophermapSection, *GophorError) {
/* Create return slice */ /* Create return slice */
sections := make([]GophermapSection, 0) sections := make([]GophermapSection, 0)
/* _Create_ hidden files map now in case dir listing requested */ /* Create hidden files map now in case dir listing requested */
hidden := make(map[string]bool) hidden := map[string]bool{
request.Path.Relative(): true, /* Ignore current gophermap */
CgiBinDirStr: true, /* Ignore cgi-bin if found */
}
/* Keep track of whether we've already come across a title line (only 1 allowed!) */ /* Keep track of whether we've already come across a title line (only 1 allowed!) */
titleAlready := false titleAlready := false
/* Reference directory listing now in case requested */ /* Error setting within nested function below */
var dirListing *GophermapDirListing var returnErr *GophorError
/* Perform buffered scan with our supplied splitter and iterators */ /* Perform buffered scan with our supplied splitter and iterators */
gophorErr := bufferedScan(path, gophorErr := bufferedScan(request.Path.Absolute(),
func(scanner *bufio.Scanner) bool { func(scanner *bufio.Scanner) bool {
line := scanner.Text() line := scanner.Text()
@ -170,12 +211,12 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
switch lineType { switch lineType {
case TypeInfoNotStated: case TypeInfoNotStated:
/* Append TypeInfo to the beginning of line */ /* Append TypeInfo to the beginning of line */
sections = append(sections, NewGophermapText(buildInfoLine(line))) sections = append(sections, &GophermapTextSection{ buildInfoLine(line) })
case TypeTitle: case TypeTitle:
/* Reformat title line to send as info line with appropriate selector */ /* Reformat title line to send as info line with appropriate selector */
if !titleAlready { if !titleAlready {
sections = append(sections, NewGophermapText(buildLine(TypeInfo, line[1:], "TITLE", NullHost, NullPort))) sections = append(sections, &GophermapTextSection{ buildLine(TypeInfo, line[1:], "TITLE", NullHost, NullPort) })
titleAlready = true titleAlready = true
} }
@ -185,59 +226,65 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
case TypeHiddenFile: case TypeHiddenFile:
/* Add to hidden files map */ /* Add to hidden files map */
hidden[line[1:]] = true hidden[request.Path.JoinRel(line[1:])] = true
case TypeSubGophermap: case TypeSubGophermap:
/* Check if we've been supplied subgophermap or regular file */ /* Parse new RequestPath and parameters */
if strings.HasSuffix(line[1:], GophermapFileStr) { subRequest, gophorErr := parseLineRequestString(request.Path, line[1:])
/* Ensure we haven't been passed the current gophermap. Recursion bad! */ if gophorErr != nil {
if line[1:] == path { /* Failed parsing line request string, set returnErr and request finish */
break returnErr = gophorErr
} return true
} else if subRequest.Path.Relative() == "" || subRequest.Path.Relative() == request.Path.Relative() {
/* Failed parsing line request string, or we've been supplied same gophermap, and recursion is
* recursion is recursion is bad kids! Set return error and request finish.
*/
returnErr = &GophorError{ InvalidRequestErr, nil }
return true
}
/* Perform file stat */
stat, err := os.Stat(subRequest.Path.Absolute())
if (err != nil) || (stat.Mode() & os.ModeDir != 0) {
/* File read error or is directory */
returnErr = &GophorError{ FileStatErr, err }
return true
}
/* Treat as any other gopher map! */ /* Check if we've been supplied subgophermap or regular file */
submapSections, gophorErr := readGophermap(line[1:]) if isGophermap(subRequest.Path.Relative()) {
if gophorErr != nil { /* If executable, store as GophermapExecFileSection, else GophermapSubmapSection */
/* Failed to read subgophermap, insert error line */ if stat.Mode().Perm() & 0100 != 0 {
sections = append(sections, NewGophermapText(buildInfoLine("Error reading subgophermap: "+line[1:]))) sections = append(sections, &GophermapExecFileSection { subRequest })
} else { } else {
sections = append(sections, submapSections...) sections = append(sections, &GophermapSubmapSection{ subRequest })
} }
} else { } else {
/* Treat as regular file, but we need to replace Unix line endings /* If stored in cgi-bin store as GophermapExecCgiSection, else GophermapFileSection */
* with gophermap line endings if withinCgiBin(subRequest.Path.Relative()) {
*/ sections = append(sections, &GophermapExecCgiSection{ subRequest })
fileContents, gophorErr := readIntoGophermap(line[1:])
if gophorErr != nil {
/* Failed to read file, insert error line */
Config.LogSystem("Error: %s\n", gophorErr)
sections = append(sections, NewGophermapText(buildInfoLine("Error reading subgophermap: "+line[1:])))
} else { } else {
sections = append(sections, NewGophermapText(fileContents)) sections = append(sections, &GophermapFileSection{ subRequest })
} }
} }
case TypeExec:
/* Try executing supplied line */
sections = append(sections, NewGophermapText(buildInfoLine("Error: inline shell commands not yet supported")))
case TypeEnd: case TypeEnd:
/* Lastline, break out at end of loop. Interface method Contents() /* Lastline, break out at end of loop. GophermapContents.Render() will
* will append a last line at the end so we don't have to worry about * append a LastLine string so we don't have to worry about that here.
* that here, only stopping the loop.
*/ */
return false return false
case TypeEndBeginList: case TypeEndBeginList:
/* Create GophermapDirListing object then break out at end of loop */ /* Append GophermapDirectorySection object then break, as with TypeEnd. */
dirListing = NewGophermapDirListing(strings.TrimSuffix(path, GophermapFileStr)) dirRequest := &Request{ NewRequestPath(request.Path.RootDir(), request.Path.TrimRelSuffix(GophermapFileStr)), "" }
sections = append(sections, &GophermapDirectorySection{ dirRequest, hidden })
return false return false
default: default:
/* Just append to sections slice as gophermap text */ /* Default is appending to sections slice as GopherMapTextSection */
sections = append(sections, NewGophermapText([]byte(line+DOSLineEnd))) sections = append(sections, &GophermapTextSection{ []byte(line+DOSLineEnd) })
} }
return true return true
}, },
) )
@ -245,26 +292,19 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
/* Check the bufferedScan didn't exit with error */ /* Check the bufferedScan didn't exit with error */
if gophorErr != nil { if gophorErr != nil {
return nil, gophorErr return nil, gophorErr
} } else if returnErr != nil {
return nil, returnErr
/* If dir listing requested, append the hidden files map then add
* to sections slice. We can do this here as the TypeEndBeginList item
* type ALWAYS comes last, at least in the gophermap handled by this call
* to readGophermap().
*/
if dirListing != nil {
dirListing.Hidden = hidden
sections = append(sections, dirListing)
} }
return sections, nil return sections, nil
} }
/* Read a text file into a gophermap as text sections */
func readIntoGophermap(path string) ([]byte, *GophorError) { func readIntoGophermap(path string) ([]byte, *GophorError) {
/* Create return slice */ /* Create return slice */
fileContents := make([]byte, 0) fileContents := make([]byte, 0)
/* Perform buffered scan with our supplied splitter and iterators */ /* Perform buffered scan with our supplied iterator */
gophorErr := bufferedScan(path, gophorErr := bufferedScan(path,
func(scanner *bufio.Scanner) bool { func(scanner *bufio.Scanner) bool {
line := scanner.Text() line := scanner.Text()
@ -274,10 +314,10 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
return true return true
} }
/* Replace the newline character */ /* Replace the newline characters */
line = strings.Replace(line, "\n", "", -1) line = replaceNewLines(line)
/* Iterate through returned str, reflowing to new line /* Iterate through line string, reflowing to new line
* until all lines < PageWidth * until all lines < PageWidth
*/ */
for len(line) > 0 { for len(line) > 0 {
@ -285,7 +325,7 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
fileContents = append(fileContents, buildInfoLine(line[:length])...) fileContents = append(fileContents, buildInfoLine(line[:length])...)
line = line[length:] line = line[length:]
} }
return true return true
}, },
) )
@ -303,6 +343,7 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
return fileContents, nil return fileContents, nil
} }
/* Return minimum width out of PageWidth and W */
func minWidth(w int) int { func minWidth(w int) int {
if w <= Config.PageWidth { if w <= Config.PageWidth {
return w return w
@ -310,9 +351,3 @@ func minWidth(w int) int {
return Config.PageWidth return Config.PageWidth
} }
} }
func replaceStrings(str string, connHost *ConnHost) []byte {
str = strings.Replace(str, ReplaceStrHostname, connHost.Name, -1)
str = strings.Replace(str, ReplaceStrPort, connHost.Port, -1)
return []byte(str)
}

@ -3,114 +3,166 @@ package main
import ( import (
"os" "os"
"sync" "sync"
"path"
"time" "time"
"strings" "regexp"
) )
type FileType int
const ( const (
/* Leads to some more concise code below */ /* Help converting file size stat to supplied size in megabytes */
FileTypeRegular FileType = iota BytesInMegaByte = 1048576.0
FileTypeDir FileType = iota
FileTypeBad FileType = iota /* Filename constants */
CgiBinDirStr = "cgi-bin"
GophermapFileStr = "gophermap"
) )
/* FileSystem:
* Object to hold and help manage our file cache. Uses a fixed map
* as a means of easily collecting files by path, but also being able
* to remove cached files in a LRU style. Uses a RW mutex to lock the
* cache map for appropriate functions and ensure thread safety.
*/
type FileSystem struct { type FileSystem struct {
/* Holds and helps manage our file cache, as well as managing
* access and responses to requests submitted a worker instance.
*/
CacheMap *FixedMap CacheMap *FixedMap
CacheMutex sync.RWMutex CacheMutex sync.RWMutex
CacheFileMax int64 CacheFileMax int64
Remaps []*FileRemap
Restricted []*regexp.Regexp
} }
func (fs *FileSystem) Init(size int, fileSizeMax float64) { func (fs *FileSystem) Init(size int, fileSizeMax float64) {
fs.CacheMap = NewFixedMap(size) fs.CacheMap = NewFixedMap(size)
fs.CacheMutex = sync.RWMutex{} fs.CacheMutex = sync.RWMutex{}
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax) fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
/* .Remaps and .Restricted are handled within gopher.go */
} }
func (fs *FileSystem) HandleRequest(requestPath string, host *ConnHost) ([]byte, *GophorError) { func (fs *FileSystem) IsRestricted(path string) bool {
/* Stat filesystem for request's file type */ for _, regex := range fs.Restricted {
fileType := FileTypeDir; if regex.MatchString(path) {
if requestPath != "/" { return true
stat, err := os.Stat(requestPath) }
if err != nil { }
/* Check file isn't in cache before throwing in the towel */ return false
fs.CacheMutex.RLock() }
file := fs.CacheMap.Get(requestPath)
if file == nil {
fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
}
/* It's there! Get contents, unlock and return */ func (fs *FileSystem) RemapRequestPath(requestPath *RequestPath) (*RequestPath, bool) {
file.Mutex.RLock() for _, remap := range fs.Remaps {
b := file.Contents(&FileSystemRequest{ requestPath, host }) /* No match :( keep lookin */
file.Mutex.RUnlock() if !remap.Regex.MatchString(requestPath.Relative()) {
continue
}
fs.CacheMutex.RUnlock() /* Create new path from template and submatches */
return b, nil newPath := make([]byte, 0)
for _, submatches := range remap.Regex.FindAllStringSubmatchIndex(requestPath.Relative(), -1) {
newPath = remap.Regex.ExpandString(newPath, remap.Template, requestPath.Relative(), submatches)
}
/* Ignore empty replacement path */
if len(newPath) == 0 {
continue
} }
/* Set file type for later handling */ /* Set this new path to the _actual_ path */
switch { return requestPath.RemapPath(string(newPath)), true
case stat.Mode() & os.ModeDir != 0: }
/* do nothing, already set :) */
break return nil, false
}
case stat.Mode() & os.ModeType == 0: func (fs *FileSystem) HandleRequest(responder *Responder) *GophorError {
fileType = FileTypeRegular /* Check if restricted file */
if fs.IsRestricted(responder.Request.Path.Relative()) {
return &GophorError{ IllegalPathErr, nil }
}
default: /* Try remap according to supplied regex */
fileType = FileTypeBad remap, doneRemap := fs.RemapRequestPath(responder.Request.Path)
var err error
var stat os.FileInfo
if doneRemap {
/* Try get the remapped path */
stat, err = os.Stat(remap.Absolute())
if err == nil {
/* Remapped path exists, set this! */
responder.Request.Path = remap
} else {
/* Last ditch effort to grab generated file */
return fs.FetchGeneratedFile(responder, err)
}
} else {
/* Just get regular supplied request path */
stat, err = os.Stat(responder.Request.Path.Absolute())
if err != nil {
/* Last ditch effort to grab generated file */
return fs.FetchGeneratedFile(responder, err)
} }
} }
switch fileType { switch {
/* Directory */ /* Directory */
case FileTypeDir: case stat.Mode() & os.ModeDir != 0:
/* Ignore anything under cgi-bin directory */
if withinCgiBin(responder.Request.Path.Relative()) {
return &GophorError{ IllegalPathErr, nil }
}
/* Check Gophermap exists */ /* Check Gophermap exists */
gophermapPath := path.Join(requestPath, GophermapFileStr) gophermapPath := NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(GophermapFileStr))
_, err := os.Stat(gophermapPath) stat, err = os.Stat(gophermapPath.Absolute())
var output []byte
var gophorErr *GophorError
if err == nil { if err == nil {
/* Gophermap exists, serve this! */ /* Gophermap exists! If executable try return executed contents, else serve as regular gophermap. */
output, gophorErr = fs.FetchFile(&FileSystemRequest{ gophermapPath, host }) gophermapRequest := &Request{ gophermapPath, responder.Request.Parameters }
responder.Request = gophermapRequest
if stat.Mode().Perm() & 0100 != 0 {
return executeFile(responder)
} else {
return fs.FetchFile(responder)
}
} else { } else {
/* No gophermap, serve directory listing */ /* No gophermap, serve directory listing */
output, gophorErr = listDir(&FileSystemRequest{ requestPath, host }, map[string]bool{}) return listDirAsGophermap(responder, map[string]bool{ gophermapPath.Relative(): true, CgiBinDirStr: true })
}
if gophorErr != nil {
/* Fail out! */
return nil, gophorErr
} }
/* Append footer text (contains last line) and return */
output = append(output, Config.FooterText...)
return output, nil
/* Regular file */ /* Regular file */
case FileTypeRegular: case stat.Mode() & os.ModeType == 0:
return fs.FetchFile(&FileSystemRequest{ requestPath, host }) /* If cgi-bin, try return executed contents. Else, fetch regular file */
if responder.Request.Path.HasRelPrefix(CgiBinDirStr) {
return executeCgi(responder)
} else {
return fs.FetchFile(responder)
}
/* Unsupported type */ /* Unsupported type */
default: default:
return nil, &GophorError{ FileTypeErr, nil } return &GophorError{ FileTypeErr, nil }
} }
} }
func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorError) { func (fs *FileSystem) FetchGeneratedFile(responder *Responder, err error) *GophorError {
fs.CacheMutex.RLock()
file := fs.CacheMap.Get(responder.Request.Path.Absolute())
if file == nil {
/* Generated file at path not in cache map either, return */
fs.CacheMutex.RUnlock()
return &GophorError{ FileStatErr, err }
}
/* It's there! Get contents! */
file.Mutex.RLock()
gophorErr := file.WriteContents(responder)
file.Mutex.RUnlock()
fs.CacheMutex.RUnlock()
return gophorErr
}
func (fs *FileSystem) FetchFile(responder *Responder) *GophorError {
/* Get cache map read lock then check if file in cache map */ /* Get cache map read lock then check if file in cache map */
fs.CacheMutex.RLock() fs.CacheMutex.RLock()
file := fs.CacheMap.Get(request.Path) file := fs.CacheMap.Get(responder.Request.Path.Absolute())
if file != nil { if file != nil {
/* File in cache -- before doing anything get file read lock */ /* File in cache -- before doing anything get file read lock */
@ -123,12 +175,12 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
file.Mutex.Lock() file.Mutex.Lock()
/* Reload file contents from disk */ /* Reload file contents from disk */
gophorErr := file.LoadContents() gophorErr := file.CacheContents()
if gophorErr != nil { if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */ /* Error loading contents, unlock all mutex then return error */
file.Mutex.Unlock() file.Mutex.Unlock()
fs.CacheMutex.RUnlock() fs.CacheMutex.RUnlock()
return nil, gophorErr return gophorErr
} }
/* Updated! Swap back file write for read lock */ /* Updated! Swap back file write for read lock */
@ -136,42 +188,48 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
file.Mutex.RLock() file.Mutex.RLock()
} }
} else { } else {
/* Perform filesystem stat ready for checking file size later. /* Open file here, to check it exists, ready for file stat
* Doing this now allows us to weed-out non-existent files early * and in case file is too big we pass it as a raw response
*/ */
stat, err := os.Stat(request.Path) fd, err := os.Open(responder.Request.Path.Absolute())
if err != nil { if err != nil {
/* Error stat'ing file, unlock read mutex then return error */ /* Error stat'ing file, unlock read mutex then return error */
fs.CacheMutex.RUnlock() fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err } return &GophorError{ FileOpenErr, err }
}
/* We need a doctor, stat! */
stat, err := fd.Stat()
if err != nil {
/* Error stat'ing file, unlock read mutext then return */
fs.CacheMutex.RUnlock()
return &GophorError{ FileStatErr, err }
}
/* Compare file size (in MB) to CacheFileSizeMax. If larger, just send file raw */
if stat.Size() > fs.CacheFileMax {
/* Unlock the read mutex, we don't need it where we're going... returning, we're returning. */
fs.CacheMutex.RUnlock()
return responder.WriteRaw(fd)
} }
/* Create new file contents object using supplied function */ /* Create new file contents */
var contents FileContents var contents FileContents
if strings.HasSuffix(request.Path, "/"+GophermapFileStr) { if isGophermap(responder.Request.Path.Relative()) {
contents = &GophermapContents{ request.Path, nil } contents = &GophermapContents{ responder.Request, nil }
} else { } else {
contents = &RegularFileContents{ request.Path, nil } contents = &RegularFileContents{ responder.Request.Path, nil }
} }
/* Create new file wrapper around contents */ /* Create new file wrapper around contents */
file = NewFile(contents) file = &File{ contents, sync.RWMutex{}, true, time.Now().UnixNano() }
/* File isn't in cache yet so no need to get file lock mutex */ /* File isn't in cache yet so no need to get file lock mutex */
gophorErr := file.LoadContents() gophorErr := file.CacheContents()
if gophorErr != nil { if gophorErr != nil {
/* Error loading contents, unlock read mutex then return error */ /* Error loading contents, unlock read mutex then return error */
fs.CacheMutex.RUnlock() fs.CacheMutex.RUnlock()
return nil, gophorErr return gophorErr
}
/* Compare file size (in MB) to CacheFileSizeMax, if larger just get file
* contents, unlock all mutex and don't bother caching.
*/
if stat.Size() > fs.CacheFileMax {
b := file.Contents(request)
fs.CacheMutex.RUnlock()
return b, nil
} }
/* File not in cache -- Swap cache map read for write lock. */ /* File not in cache -- Swap cache map read for write lock. */
@ -179,7 +237,7 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
fs.CacheMutex.Lock() fs.CacheMutex.Lock()
/* Put file in the FixedMap */ /* Put file in the FixedMap */
fs.CacheMap.Put(request.Path, file) fs.CacheMap.Put(responder.Request.Path.Absolute(), file)
/* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */ /* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */
file.Mutex.RLock() file.Mutex.RLock()
@ -189,84 +247,48 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
fs.CacheMutex.RLock() fs.CacheMutex.RLock()
} }
/* Read file contents into new variable for return, then unlock file read lock */ /* Write file contents via responder */
b := file.Contents(request) gophorErr := file.WriteContents(responder)
file.Mutex.RUnlock() file.Mutex.RUnlock()
/* Finally we can unlock the cache map read lock, we are done :) */ /* Finally we can unlock the cache map read lock, we are done :) */
fs.CacheMutex.RUnlock() fs.CacheMutex.RUnlock()
return b, nil return gophorErr
}
/* FileSystemRequest:
* Makes a request to the filesystem either through
* the FileCache or directly to a function like listDir().
* It carries the requested filesystem path and any extra
* needed information, for the moment just a set of details
* about the virtual host.. Opens things up a lot more for
* the future :)
*/
type FileSystemRequest struct {
Path string
Host *ConnHost
} }
/* File:
* Wraps around the cached contents of a file and
* helps with management of this content by the
* global FileCache objects.
*/
type File struct { type File struct {
contents FileContents /* Wraps around the cached contents of a file
* helping with management.
*/
Content FileContents
Mutex sync.RWMutex Mutex sync.RWMutex
Fresh bool Fresh bool
LastRefresh int64 LastRefresh int64
} }
func NewFile(contents FileContents) *File { func (f *File) WriteContents(responder *Responder) *GophorError {
return &File{ return f.Content.Render(responder)
contents,
sync.RWMutex{},
true,
0,
}
}
func (f *File) Contents(request *FileSystemRequest) []byte {
return f.contents.Render(request)
} }
func (f *File) LoadContents() *GophorError { func (f *File) CacheContents() *GophorError {
/* Clear current file contents */ /* Clear current file contents */
f.contents.Clear() f.Content.Clear()
/* Reload the file */ /* Reload the file */
gophorErr := f.contents.Load() gophorErr := f.Content.Load()
if gophorErr != nil { if gophorErr != nil {
return gophorErr return gophorErr
} }
/* Update lastRefresh, set fresh, unset deletion (not likely set) */ /* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.LastRefresh = time.Now().UnixNano() f.LastRefresh = time.Now().UnixNano()
f.Fresh = true f.Fresh = true
return nil return nil
} }
/* FileContents: /* Start the file monitor! */
* Interface that provides an adaptable implementation
* for holding onto some level of information about
* the contents of a file, also methods for processing
* and returning the results when the file contents
* are requested.
*/
type FileContents interface {
Render(*FileSystemRequest) []byte
Load() *GophorError
Clear()
}
func startFileMonitor(sleepTime time.Duration) { func startFileMonitor(sleepTime time.Duration) {
go func() { go func() {
for { for {
@ -278,10 +300,11 @@ func startFileMonitor(sleepTime time.Duration) {
} }
/* We shouldn't have reached here */ /* We shouldn't have reached here */
Config.LogSystemFatal("FileCache monitor escaped run loop!\n") Config.SysLog.Fatal("", "FileCache monitor escaped run loop!\n")
}() }()
} }
/* Check file cache for freshness, deleting files not-on disk */
func checkCacheFreshness() { func checkCacheFreshness() {
/* Before anything, get cache write lock (in case we have to delete) */ /* Before anything, get cache write lock (in case we have to delete) */
Config.FileSystem.CacheMutex.Lock() Config.FileSystem.CacheMutex.Lock()
@ -296,16 +319,18 @@ func checkCacheFreshness() {
continue continue
} }
/* Check file still exists on disk, delete and continue if not */
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err != nil { if err != nil {
/* Log file as not in cache, then delete */ Config.SysLog.Error("", "Failed to stat file in cache: %s\n", path)
Config.LogSystemError("Failed to stat file in cache: %s\n", path)
Config.FileSystem.CacheMap.Remove(path) Config.FileSystem.CacheMap.Remove(path)
continue continue
} }
/* Get file's last modified time */
timeModified := stat.ModTime().UnixNano() timeModified := stat.ModTime().UnixNano()
/* If the file is marked as fresh, but file on disk newer, mark as unfresh */ /* If the file is marked as fresh, but file on disk is newer, mark as unfresh */
if file.Fresh && file.LastRefresh < timeModified { if file.Fresh && file.LastRefresh < timeModified {
file.Fresh = false file.Fresh = false
} }
@ -315,9 +340,9 @@ func checkCacheFreshness() {
Config.FileSystem.CacheMutex.Unlock() Config.FileSystem.CacheMutex.Unlock()
} }
/* Just a helper function to neaten-up checking if file contents is of generated type */
func isGeneratedType(file *File) bool { func isGeneratedType(file *File) bool {
/* Just a helper function to neaten-up checking if file contents is of generated type */ switch file.Content.(type) {
switch file.contents.(type) {
case *GeneratedFileContents: case *GeneratedFileContents:
return true return true
default: default:

@ -2,13 +2,16 @@ package main
import ( import (
"os" "os"
"path"
"bytes" "bytes"
"io" "io"
"sort" "sort"
"bufio" "bufio"
) )
const (
FileReadBufSize = 1024
)
/* Perform simple buffered read on a file at path */ /* Perform simple buffered read on a file at path */
func bufferedRead(path string) ([]byte, *GophorError) { func bufferedRead(path string) ([]byte, *GophorError) {
/* Open file */ /* Open file */
@ -77,6 +80,7 @@ func bufferedScan(path string, scanIterator func(*bufio.Scanner) bool) *GophorEr
return nil return nil
} }
/* Split on DOS line end */
func dosLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) { func dosLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 { if atEOF && len(data) == 0 {
/* At EOF, no more data */ /* At EOF, no more data */
@ -92,6 +96,7 @@ func dosLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err
return 0, nil, nil return 0, nil, nil
} }
/* Split on unix line end */
func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) { func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 { if atEOF && len(data) == 0 {
/* At EOF, no more data */ /* At EOF, no more data */
@ -107,100 +112,87 @@ func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, er
return 0, nil, nil return 0, nil, nil
} }
/* listDir(): /* List the files in directory, hiding those requested, including title and footer */
* Here we use an empty function pointer, and set the correct func listDirAsGophermap(responder *Responder, hidden map[string]bool) *GophorError {
* function to be used during the restricted files regex parsing. /* Write title */
* This negates need to check if RestrictedFilesRegex is nil every gophorErr := responder.WriteData(append(buildLine(TypeInfo, "[ "+responder.Host.Name()+responder.Request.Path.Selector()+" ]", "TITLE", NullHost, NullPort), buildInfoLine("")...))
* single call. if gophorErr != nil {
*/ return gophorErr
var listDir func(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) }
func _listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If requested hidden */
if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := path.Join(request.Path, file.Name())
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := path.Join(request.Path, file.Name())
itemType := getItemType(itemPath)
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
})
}
func _listDirRegexMatch(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If regex match in restricted files || requested hidden */
if isRestrictedFile(file.Name()) {
return
} else if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */ /* Writer a 'back' entry. GoLang Readdir() seems to miss this */
switch { gophorErr = responder.WriteData(buildLine(TypeDirectory, "..", responder.Request.Path.JoinSelector(".."), responder.Host.Name(), responder.Host.Port()))
case file.Mode() & os.ModeDir != 0: if gophorErr != nil {
/* Directory -- create directory listing */ return gophorErr
itemPath := path.Join(request.Path, file.Name()) }
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0: /* Write the actual directory entry */
/* Regular file -- find item type and creating listing */ gophorErr = listDir(responder, hidden)
itemPath := path.Join(request.Path, file.Name()) if gophorErr != nil {
itemType := getItemType(itemPath) return gophorErr
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...) }
default: /* Finally write footer */
/* Ignore */ return responder.WriteData(Config.FooterText)
}
})
} }
func _listDirBase(request *FileSystemRequest, iterFunc func(dirContents *[]byte, file os.FileInfo)) ([]byte, *GophorError) { /* List the files in a directory, hiding those requested */
func listDir(responder *Responder, hidden map[string]bool) *GophorError {
/* Open directory file descriptor */ /* Open directory file descriptor */
fd, err := os.Open(request.Path) fd, err := os.Open(responder.Request.Path.Absolute())
if err != nil { if err != nil {
Config.LogSystemError("failed to open %s: %s\n", request.Path, err.Error()) Config.SysLog.Error("", "failed to open %s: %s\n", responder.Request.Path.Absolute(), err.Error())
return nil, &GophorError{ FileOpenErr, err } return &GophorError{ FileOpenErr, err }
} }
/* Read files in directory */ /* Read files in directory */
files, err := fd.Readdir(-1) files, err := fd.Readdir(-1)
if err != nil { if err != nil {
Config.LogSystemError("failed to enumerate dir %s: %s\n", request.Path, err.Error()) Config.SysLog.Error("", "failed to enumerate dir %s: %s\n", responder.Request.Path.Absolute(), err.Error())
return nil, &GophorError{ DirListErr, err } return &GophorError{ DirListErr, err }
} }
/* Sort the files by name */ /* Sort the files by name */
sort.Sort(byName(files)) sort.Sort(byName(files))
/* Create directory content slice, ready */ /* Create directory content slice, ready */
dirContents := make([]byte, 0) dirContents := make([]byte, 0)
/* First add a title + a space */ /* Walk through files :D */
dirContents = append(dirContents, buildLine(TypeInfo, "[ "+request.Host.Name+request.Path+" ]", "TITLE", NullHost, NullPort)...) var reqPath *RequestPath
dirContents = append(dirContents, buildInfoLine("")...) for _, file := range files {
reqPath = NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(file.Name()))
/* Add a 'back' entry. GoLang Readdir() seems to miss this */ /* If hidden file, or restricted file, continue! */
dirContents = append(dirContents, buildLine(TypeDirectory, "..", path.Join(fd.Name(), ".."), request.Host.Name, request.Host.Port)...) if isHiddenFile(hidden, reqPath.Relative()) /*|| isRestrictedFile(reqPath.Relative())*/ {
continue
}
/* Walk through files :D */ /* Handle file, directory or ignore others */
for _, file := range files { iterFunc(&dirContents, file) } switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
dirContents = append(dirContents, buildLine(TypeDirectory, file.Name(), reqPath.Selector(), responder.Host.Name(), responder.Host.Port())...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := reqPath.Selector()
itemType := getItemType(itemPath)
dirContents = append(dirContents, buildLine(itemType, file.Name(), reqPath.Selector(), responder.Host.Name(), responder.Host.Port())...)
default:
/* Ignore */
}
}
/* Finally write dirContents and return result */
return responder.WriteData(dirContents)
}
return dirContents, nil /* Helper function to simple checking in map */
func isHiddenFile(hiddenMap map[string]bool, fileName string) bool {
_, ok := hiddenMap[fileName]
return ok
} }
/* Took a leaf out of go-gopher's book here. */ /* Took a leaf out of go-gopher's book here. */

@ -38,14 +38,15 @@ func NewFixedMap(size int) *FixedMap {
func (fm *FixedMap) Get(key string) *File { func (fm *FixedMap) Get(key string) *File {
elem, ok := fm.Map[key] elem, ok := fm.Map[key]
if ok { if ok {
/* And that's an LRU implementation folks! */
fm.List.MoveToFront(elem.Element)
return elem.Value return elem.Value
} else { } else {
return nil return nil
} }
} }
/* Put file in map as key, pushing out last file /* Put file in map as key, pushing out last file if size limit reached */
* if size limit reached */
func (fm *FixedMap) Put(key string, value *File) { func (fm *FixedMap) Put(key string, value *File) {
element := fm.List.PushFront(key) element := fm.List.PushFront(key)
fm.Map[key] = &MapElement{ element, value } fm.Map[key] = &MapElement{ element, value }
@ -60,8 +61,6 @@ func (fm *FixedMap) Put(key string, value *File) {
/* Finally delete the map entry and list element! */ /* Finally delete the map entry and list element! */
delete(fm.Map, key) delete(fm.Map, key)
fm.List.Remove(element) fm.List.Remove(element)
Config.LogSystem("Popped key: %s\n", key)
} }
} }

@ -4,152 +4,6 @@ import (
"strings" "strings"
) )
var FileExtMap = map[string]ItemType{
".out": TypeBin,
".a": TypeBin,
".o": TypeBin,
".ko": TypeBin, /* ... Though tbh, kernel extensions?!!! */
".msi": TypeBin,
".exe": TypeBin,
".lz": TypeBinArchive,
".gz": TypeBinArchive,
".bz2": TypeBinArchive,
".7z": TypeBinArchive,
".zip": TypeBinArchive,
".gitignore": TypeFile,
".txt": TypeFile,
".json": TypeFile,
".yaml": TypeFile,
".ocaml": TypeFile,
".s": TypeFile,
".c": TypeFile,
".py": TypeFile,
".h": TypeFile,
".go": TypeFile,
".fs": TypeFile,
".odin": TypeFile,
".nanorc": TypeFile,
".bashrc": TypeFile,
".mkshrc": TypeFile,
".vimrc": TypeFile,
".vim": TypeFile,
".viminfo": TypeFile,
".sh": TypeFile,
".conf": TypeFile,
".xinitrc": TypeFile,
".jstarrc": TypeFile,
".joerc": TypeFile,
".jpicorc": TypeFile,
".profile": TypeFile,
".bash_profile": TypeFile,
".bash_logout": TypeFile,
".log": TypeFile,
".ovpn": TypeFile,
".md": TypeMarkup,
".xml": TypeXml,
".doc": TypeDoc,
".docx": TypeDoc,
".pdf": TypeDoc,
".jpg": TypeImage,
".jpeg": TypeImage,
".png": TypeImage,
".gif": TypeImage,
".html": TypeHtml,
".htm": TypeHtml,
".ogg": TypeAudio,
".mp3": TypeAudio,
".wav": TypeAudio,
".mod": TypeAudio,
".it": TypeAudio,
".xm": TypeAudio,
".mid": TypeAudio,
".vgm": TypeAudio,
".opus": TypeAudio,
".m4a": TypeAudio,
".aac": TypeAudio,
".mp4": TypeVideo,
".mkv": TypeVideo,
".webm": TypeVideo,
}
func buildError(selector string) []byte {
ret := string(TypeError)
ret += selector + DOSLineEnd
ret += LastLine
return []byte(ret)
}
/* Build gopher compliant line with supplied information */
func buildLine(t ItemType, name, selector, host string, port string) []byte {
ret := string(t)
/* Add name, truncate name if too long */
if len(name) > Config.PageWidth {
ret += name[:Config.PageWidth-5]+"...\t"
} else {
ret += name+"\t"
}
/* Add selector. If too long use err, skip if empty */
selectorLen := len(selector)
if selectorLen > MaxSelectorLen {
ret += SelectorErrorStr+"\t"
} else if selectorLen > 0 {
ret += selector+"\t"
}
/* Add host + port */
ret += host+"\t"+port+DOSLineEnd
return []byte(ret)
}
/* Build gopher compliant info line */
func buildInfoLine(content string) []byte {
return buildLine(TypeInfo, content, NullSelector, NullHost, NullPort)
}
/* Get item type for named file on disk */
func getItemType(name string) ItemType {
/* Split, name MUST be lower */
split := strings.Split(strings.ToLower(name), ".")
/* First we look at how many '.' in name string */
splitLen := len(split)
switch splitLen {
case 0:
/* Always return TypeDefault. We can never tell */
return TypeDefault
default:
/* Get index of str after last ".", look in FileExtMap */
fileType, ok := FileExtMap["."+split[splitLen-1]]
if ok {
return fileType
} else {
return TypeDefault
}
}
}
/* Build a line separator of supplied width */
func buildLineSeparator(count int) string {
ret := ""
for i := 0; i < count; i += 1 {
ret += "_"
}
return ret
}
/* Formats an info-text footer from string. Add last line as we use the footer to contain last line (regardless if empty) */ /* Formats an info-text footer from string. Add last line as we use the footer to contain last line (regardless if empty) */
func formatGophermapFooter(text string, useSeparator bool) []byte { func formatGophermapFooter(text string, useSeparator bool) []byte {
ret := make([]byte, 0) ret := make([]byte, 0)
@ -162,51 +16,35 @@ func formatGophermapFooter(text string, useSeparator bool) []byte {
ret = append(ret, buildInfoLine(line)...) ret = append(ret, buildInfoLine(line)...)
} }
} }
ret = append(ret, []byte(LastLine)...) return append(ret, []byte(LastLine)...)
return ret
} }
/* Parse line type from contents */ /* Replace standard replacement strings */
func parseLineType(line string) ItemType { func replaceStrings(str string, connHost *ConnHost) []byte {
lineLen := len(line) /* We only replace the actual host and port values */
split := strings.Split(str, Tab)
if len(split) < 4 {
return []byte(str)
}
if lineLen == 0 { split[2] = strings.Replace(split[2], ReplaceStrHostname, connHost.Name(), -1)
return TypeInfoNotStated split[3] = strings.Replace(split[3], ReplaceStrPort, connHost.Port(), -1)
} else if lineLen == 1 {
/* The only accepted types for a length 1 line */ /* Return slice */
switch ItemType(line[0]) { b := make([]byte, 0)
case TypeEnd:
return TypeEnd /* Recombine the slices and add the removed tabs */
case TypeEndBeginList: splitLen := len(split)
return TypeEndBeginList for i := 0; i < splitLen-1; i += 1 {
case TypeComment: split[i] += Tab
return TypeComment b = append(b, []byte(split[i])...)
case TypeInfo:
return TypeInfo
case TypeTitle:
return TypeTitle
default:
return TypeUnknown
}
} else if !strings.Contains(line, string(Tab)) {
/* The only accepted types for a line with no tabs */
switch ItemType(line[0]) {
case TypeComment:
return TypeComment
case TypeTitle:
return TypeTitle
case TypeInfo:
return TypeInfo
case TypeHiddenFile:
return TypeHiddenFile
case TypeSubGophermap:
return TypeSubGophermap
case TypeExec:
return TypeExec
default:
return TypeInfoNotStated
}
} }
b = append(b, []byte(split[splitLen-1])...)
return b
}
return ItemType(line[0]) /* Replace new-line characters */
func replaceNewLines(str string) string {
return strings.Replace(str, "\n", "", -1)
} }

@ -0,0 +1,237 @@
package main
import (
"strings"
)
type GopherUrl struct {
Path string
Parameters string
}
const (
/* Just naming some constants */
DOSLineEnd = "\r\n"
UnixLineEnd = "\n"
End = "."
Tab = "\t"
LastLine = End+DOSLineEnd
/* Gopher line formatting */
MaxUserNameLen = 70 /* RFC 1436 standard, though we use user-supplied page-width */
MaxSelectorLen = 255 /* RFC 1436 standard */
SelectorErrorStr = "/max_selector_length_reached"
GophermapRenderErrorStr = ""
GophermapReadErrorStr = "Error reading subgophermap: "
GophermapExecErrorStr = "Error executing gophermap: "
/* Default null values */
NullSelector = "-"
NullHost = "null.host"
NullPort = "0"
/* Replacement strings */
ReplaceStrHostname = "$hostname"
ReplaceStrPort = "$port"
)
/*
* Item type characters:
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
* and Gophernicus project. Those with ALL-CAPS descriptions in
* [square brackets] defined and used by Gophernicus, a popular
* Gopher server.
*/
type ItemType byte
const (
/* RFC 1436 Standard */
TypeFile = ItemType('0') /* Regular file (text) */
TypeDirectory = ItemType('1') /* Directory (menu) */
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
TypeError = ItemType('3') /* Error message */
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
TypeSearch = ItemType('7') /* Query search engine or CGI script */
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
TypeGif = ItemType('g') /* GIF format image file (just use I) */
TypeImage = ItemType('I') /* Any format image file */
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
/* GopherII Standard */
TypeCalendar = ItemType('c') /* Calendar file */
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
TypeHtml = ItemType('h') /* HTML document */
TypeInfo = ItemType('i') /* Informational text (not selectable) */
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
TypeMail = ItemType('M') /* Email repository (MBOX) */
TypeAudio = ItemType('s') /* Audio recordings */
TypeXml = ItemType('x') /* eXtensible Markup Language document */
TypeVideo = ItemType(';') /* Video files */
/* Commonly Used */
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on directory listing */
/* Default type */
TypeDefault = TypeBin
/* Gophor specific types */
TypeInfoNotStated = ItemType('I') /* [INTERNAL USE] */
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
)
var FileExtMap = map[string]ItemType{
".out": TypeBin,
".a": TypeBin,
".o": TypeBin,
".ko": TypeBin, /* ... Though tbh, kernel extensions?!!! */
".msi": TypeBin,
".exe": TypeBin,
".gophermap": TypeDirectory,
".lz": TypeBinArchive,
".gz": TypeBinArchive,
".bz2": TypeBinArchive,
".7z": TypeBinArchive,
".zip": TypeBinArchive,
".gitignore": TypeFile,
".txt": TypeFile,
".json": TypeFile,
".yaml": TypeFile,
".ocaml": TypeFile,
".s": TypeFile,
".c": TypeFile,
".py": TypeFile,
".h": TypeFile,
".go": TypeFile,
".fs": TypeFile,
".odin": TypeFile,
".nanorc": TypeFile,
".bashrc": TypeFile,
".mkshrc": TypeFile,
".vimrc": TypeFile,
".vim": TypeFile,
".viminfo": TypeFile,
".sh": TypeFile,
".conf": TypeFile,
".xinitrc": TypeFile,
".jstarrc": TypeFile,
".joerc": TypeFile,
".jpicorc": TypeFile,
".profile": TypeFile,
".bash_profile": TypeFile,
".bash_logout": TypeFile,
".log": TypeFile,
".ovpn": TypeFile,
".md": TypeMarkup,
".xml": TypeXml,
".doc": TypeDoc,
".docx": TypeDoc,
".pdf": TypeDoc,
".jpg": TypeImage,
".jpeg": TypeImage,
".png": TypeImage,
".gif": TypeImage,
".html": TypeHtml,
".htm": TypeHtml,
".ogg": TypeAudio,
".mp3": TypeAudio,
".wav": TypeAudio,
".mod": TypeAudio,
".it": TypeAudio,
".xm": TypeAudio,
".mid": TypeAudio,
".vgm": TypeAudio,
".opus": TypeAudio,
".m4a": TypeAudio,
".aac": TypeAudio,
".mp4": TypeVideo,
".mkv": TypeVideo,
".webm": TypeVideo,
}
/* Build error line */
func buildErrorLine(selector string) []byte {
ret := string(TypeError)
ret += selector + DOSLineEnd
ret += LastLine
return []byte(ret)
}
/* Build gopher compliant line with supplied information */
func buildLine(t ItemType, name, selector, host string, port string) []byte {
ret := string(t)
/* Add name, truncate name if too long */
if len(name) > Config.PageWidth {
ret += name[:Config.PageWidth-5]+"..."+Tab
} else {
ret += name+Tab
}
/* Add selector. If too long use err, skip if empty */
selectorLen := len(selector)
if selectorLen > MaxSelectorLen {
ret += SelectorErrorStr+Tab
} else if selectorLen > 0 {
ret += selector+Tab
}
/* Add host + port */
ret += host+Tab+port+DOSLineEnd
return []byte(ret)
}
/* Build gopher compliant info line */
func buildInfoLine(content string) []byte {
return buildLine(TypeInfo, content, NullSelector, NullHost, NullPort)
}
/* Get item type for named file on disk */
func getItemType(name string) ItemType {
/* Split, name MUST be lower */
split := strings.Split(strings.ToLower(name), ".")
/* First we look at how many '.' in name string */
splitLen := len(split)
switch splitLen {
case 0:
/* Always return TypeDefault. We can never tell */
return TypeDefault
default:
/* Get index of str after last ".", look in FileExtMap */
fileType, ok := FileExtMap["."+split[splitLen-1]]
if ok {
return fileType
} else {
return TypeDefault
}
}
}
/* Build a line separator of supplied width */
func buildLineSeparator(count int) string {
ret := ""
for i := 0; i < count; i += 1 {
ret += "_"
}
return ret
}

@ -2,7 +2,7 @@ package main
import ( import (
"os" "os"
"os/user" "log"
"strconv" "strconv"
"syscall" "syscall"
"os/signal" "os/signal"
@ -10,23 +10,9 @@ import (
"time" "time"
) )
/* const (
* GoLang's built-in syscall.{Setuid,Setgid}() methods don't work as expected (all I ever GophorVersion = "1.0-beta"
* run into is 'operation not supported'). Which from reading seems to be a result of Linux )
* not always performing setuid/setgid constistent with the Unix expected result. Then mix
* that with GoLang's goroutines acting like threads but not quite the same... I can see
* why they're not fully supported.
*
* Instead we're going to take C-bindings and call them directly ourselves, BEFORE spawning
* any goroutines to prevent fuckery.
*
* Oh god here we go...
*/
/*
#include <unistd.h>
*/
import "C"
var ( var (
Config *ServerConfig Config *ServerConfig
@ -38,31 +24,27 @@ func main() {
/* Handle signals so we can _actually_ shutdowm */ /* Handle signals so we can _actually_ shutdowm */
signals := make(chan os.Signal) signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
/* Start accepting connections on any supplied listeners */ /* Start accepting connections on any supplied listeners */
for _, l := range listeners { for _, l := range listeners {
go func() { go func() {
Config.LogSystem("Listening on: gopher://%s\n", l.Addr()) Config.SysLog.Info("", "Listening on: gopher://%s:%s\n", l.Host.Name(), l.Host.RealPort())
for { for {
newConn, err := l.Accept() worker, err := l.Accept()
if err != nil { if err != nil {
Config.LogSystemError("Error accepting connection: %s\n", err.Error()) Config.SysLog.Error("", "Error accepting connection: %s\n", err.Error())
continue continue
} }
go worker.Serve()
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
NewWorker(newConn).Serve()
}()
} }
}() }()
} }
/* When OS signal received, we close-up */ /* When OS signal received, we close-up */
sig := <-signals sig := <-signals
Config.LogSystem("Signal received: %v. Shutting down...\n", sig) Config.SysLog.Info("", "Signal received: %v. Shutting down...\n", sig)
os.Exit(0) os.Exit(0)
} }
@ -70,37 +52,63 @@ func setupServer() []*GophorListener {
/* First we setup all the flags and parse them... */ /* First we setup all the flags and parse them... */
/* Base server settings */ /* Base server settings */
serverRoot := flag.String("root", "/var/gopher", "Change server root directory.") serverRoot := flag.String("root", "/var/gopher", "Change server root directory.")
serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).") serverBindAddr := flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
serverPort := flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).") serverPort := flag.Int("port", 70, "Change server bind port.")
serverBindAddr := flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
execAs := flag.String("user", "", "Drop to supplied user's UID and GID permissions before execution.")
/* User supplied caps.txt information */ serverFwdPort := flag.Int("fwd-port", 0, "Change port used in '$port' replacement strings (useful if you're port forwarding).")
serverDescription := flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in generated caps.txt.") serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.") /* Logging settings */
systemLogPath := flag.String("system-log", "", "Change server system log file (blank outputs to stderr).")
accessLogPath := flag.String("access-log", "", "Change server access log file (blank outputs to stderr).")
logOutput := flag.String("log-output", "stderr", "Change server log file handling (disable|stderr|file)")
logOpts := flag.String("log-opts", "timestamp,ip", "Comma-separated list of log options (timestamp|ip)")
/* File system */
fileMonitorFreq := flag.Duration("file-monitor-freq", time.Second*60, "Change file monitor frequency.")
/* Cache settings */
cacheSize := flag.Int("cache-size", 50, "Change file cache size, measured in file count.")
cacheFileSizeMax := flag.Float64("cache-file-max", 0.5, "Change maximum file size to be cached (in megabytes).")
cacheDisabled := flag.Bool("disable-cache", false, "Disable file caching.")
/* Content settings */ /* Content settings */
footerText := flag.String("footer", "", "Change gophermap footer text (Unix new-line separated lines).") pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.") // charSet := flag.String("charset", "", "Change default output charset.")
charSet := "utf-8"
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.") footerText := flag.String("footer", " Gophor, a Gopher server in Go.", "Change gophermap footer text (Unix new-line separated lines).")
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting files from showing in directory listings.") footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
/* Logging settings */ /* Regex */
systemLogPath := flag.String("system-log", "", "Change server system log file (blank outputs to stderr).") restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting accessible files.")
accessLogPath := flag.String("access-log", "", "Change server access log file (blank outputs to stderr).") fileRemaps := flag.String("file-remap", "", "New-line separated list of file remappings of format: /virtual/relative/path -> /actual/relative/path")
logType := flag.Int("log-type", 0, "Change server log file handling -- 0:default 1:disable")
/* Cache settings */ /* User supplied caps.txt information */
cacheCheckFreq := flag.String("cache-check", "60s", "Change file cache freshness check frequency.") serverDescription := flag.String("description", "Gophor, a Gopher server in Go.", "Change server description in generated caps.txt.")
cacheSize := flag.Int("cache-size", 50, "Change file cache size, measured in file count.") serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
cacheFileSizeMax := flag.Float64("cache-file-max", 0.5, "Change maximum file size to be cached (in megabytes).") serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.")
cacheDisabled := flag.Bool("disable-cache", false, "Disable file caching.")
/* Exec settings */
disableCgi := flag.Bool("disable-cgi", false, "Disable CGI and all executable support.")
httpCompatCgi := flag.Bool("http-compat-cgi", false, "Enable HTTP CGI script compatibility (will strip HTTP headers).")
httpHeaderBuf := flag.Int("http-header-buf", 4096, "Change max CGI read count to look for and strip HTTP headers before sending raw (bytes).")
safeExecPath := flag.String("safe-path", "/usr/bin:/bin", "Set safe PATH variable to be used when executing CGI scripts, gophermaps and inline shell commands.")
maxExecRunTime := flag.Duration("max-exec-time", time.Second*3, "Change max executable CGI, gophermap and inline shell command runtime.")
/* Buffer sizes */
socketWriteBuf := flag.Int("socket-write-buf", 4096, "Change socket write buffer size (bytes).")
socketReadBuf := flag.Int("socket-read-buf", 256, "Change socket read buffer size (bytes).")
socketReadMax := flag.Int("socket-read-max", 8, "Change socket read count max (integer multiplier socket-read-buf-max)")
fileReadBuf := flag.Int("file-read-buf", 4096, "Change file read buffer size (bytes).")
/* Socket deadliens */
socketReadTimeout := flag.Duration("socket-read-timeout", time.Second*5, "Change socket read deadline (timeout).")
socketWriteTimeout := flag.Duration("socket-write-timeout", time.Second*30, "Change socket write deadline (timeout).")
/* Version string */ /* Version string */
version := flag.Bool("version", false, "Print version information.") version := flag.Bool("version", false, "Print version information.")
/* Parse parse parse!! */ /* Parse parse parse!! */
flag.Parse() flag.Parse()
@ -110,104 +118,117 @@ func setupServer() []*GophorListener {
/* Setup the server configuration instance and enter as much as we can right now */ /* Setup the server configuration instance and enter as much as we can right now */
Config = new(ServerConfig) Config = new(ServerConfig)
Config.RootDir = *serverRoot
Config.PageWidth = *pageWidth /* Set misc content settings */
Config.PageWidth = *pageWidth
/* Setup various buffer sizes */
Config.SocketWriteBufSize = *socketWriteBuf
Config.SocketReadBufSize = *socketReadBuf
Config.SocketReadMax = *socketReadBuf * *socketReadMax
Config.FileReadBufSize = *fileReadBuf
/* Setup socket deadlines */
Config.SocketReadDeadline = *socketReadTimeout
Config.SocketWriteDeadline = *socketWriteTimeout
/* Have to be set AFTER page width variable set */ /* Have to be set AFTER page width variable set */
Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator) Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator)
/* Setup Gophor logging system */ /* Setup Gophor logging system */
Config.SystemLogger, Config.AccessLogger = setupLogging(*logType, *systemLogPath, *accessLogPath) Config.SysLog, Config.AccLog = setupLoggers(*logOutput, *logOpts, *systemLogPath, *accessLogPath)
/* Get UID + GID for requested user. Has to be done BEFORE chroot or it fails */ /* Set CGI support status */
var uid, gid int if *disableCgi {
if *execAs == "" { Config.SysLog.Info("", "CGI support disabled\n")
/* No 'execAs' user specified, try run as default user account permissions */ Config.CgiEnabled = false
uid = 1000
gid = 1000
} else if *execAs == "root" {
/* Naughty, naughty! */
Config.LogSystemFatal("Gophor does not support directly running as root\n")
} else { } else {
/* Try lookup specified username */ /* Enable CGI */
user, err := user.Lookup(*execAs) Config.SysLog.Info("", "CGI support enabled\n")
if err != nil { Config.CgiEnabled = true
Config.LogSystemFatal("Error getting information for requested user %s: %s\n", *execAs, err)
if *httpCompatCgi {
Config.SysLog.Info("", "Enabling HTTP CGI script compatibility\n")
executeCgi = executeCgiStripHttp
/* Specific to CGI buffer */
Config.SysLog.Info("", "Max CGI HTTP header read-ahead: %d bytes\n", *httpHeaderBuf)
Config.SkipPrefixBufSize = *httpHeaderBuf
} else {
executeCgi = executeCgiNoHttp
} }
/* These values should be coming straight out of /etc/passwd, so assume safe */ /* Set safe executable path and setup environments */
uid, _ = strconv.Atoi(user.Uid) Config.SysLog.Info("", "Setting safe executable path: %s\n", *safeExecPath)
gid, _ = strconv.Atoi(user.Gid) Config.Env = setupExecEnviron(*safeExecPath)
Config.CgiEnv = setupInitialCgiEnviron(*safeExecPath, charSet)
/* Set executable watchdog */
Config.SysLog.Info("", "Max executable time: %s\n", *maxExecRunTime)
Config.MaxExecRunTime = *maxExecRunTime
}
/* If running as root, get ready to drop privileges */
if syscall.Getuid() == 0 || syscall.Getgid() == 0 {
log.Fatalf("", "Gophor does not support running as root!\n")
} }
/* Enter server dir */ /* Enter server dir */
enterServerDir(*serverRoot) enterServerDir(*serverRoot)
Config.LogSystem("Entered server directory: %s\n", *serverRoot) Config.SysLog.Info("", "Entered server directory: %s\n", *serverRoot)
/* Try enter chroot if requested */
chrootServerDir(*serverRoot)
Config.LogSystem("Chroot success, new root: %s\n", *serverRoot)
/* Setup listeners */ /* Setup listeners */
listeners := make([]*GophorListener, 0) listeners := make([]*GophorListener, 0)
/* If requested, setup unencrypted listener */ /* If requested, setup unencrypted listener */
if *serverPort != 0 { if *serverPort != 0 {
l, err := BeginGophorListen(*serverBindAddr, *serverHostname, strconv.Itoa(*serverPort)) /* If no forward port set, just use regular */
if *serverFwdPort == 0 {
*serverFwdPort = *serverPort
}
l, err := BeginGophorListen(*serverBindAddr, *serverHostname, strconv.Itoa(*serverPort), strconv.Itoa(*serverFwdPort), *serverRoot)
if err != nil { if err != nil {
Config.LogSystemFatal("Error setting up (unencrypted) listener: %s\n", err.Error()) log.Fatalf("Error setting up (unencrypted) listener: %s\n", err.Error())
} }
listeners = append(listeners, l) listeners = append(listeners, l)
} else { } else {
Config.LogSystemFatal("No valid port to listen on :(\n") log.Fatalf("No valid port to listen on\n")
}
/* Drop privileges to retrieved UID + GID */
setPrivileges(uid, gid)
Config.LogSystem("Successfully dropped privileges to UID:%d GID:%d\n", uid, gid)
/* Compile user restricted files regex if supplied */
if *restrictedFiles != "" {
Config.RestrictedFiles = compileUserRestrictedFilesRegex(*restrictedFiles)
/* Setup the listDir function to use regex matching */
listDir = _listDirRegexMatch
} else {
/* Setup the listDir function to skip regex matching */
listDir = _listDir
} }
/* Setup file cache */ /* Setup file cache */
Config.FileSystem = new(FileSystem) Config.FileSystem = new(FileSystem)
/* Check if cache requested disabled */
if !*cacheDisabled { if !*cacheDisabled {
/* Parse suppled cache check frequency time */
fileMonitorSleepTime, err := time.ParseDuration(*cacheCheckFreq)
if err != nil {
Config.LogSystemFatal("Error parsing supplied cache check frequency %s: %s\n", *cacheCheckFreq, err)
}
/* Init file cache */ /* Init file cache */
Config.FileSystem.Init(*cacheSize, *cacheFileSizeMax) Config.FileSystem.Init(*cacheSize, *cacheFileSizeMax)
Config.LogSystem("File caching enabled with: maxcount=%d maxsize=%.3fMB\n", *cacheSize, *cacheFileSizeMax)
/* Before file monitor or any kind of new goroutines started, /* Before file monitor or any kind of new goroutines started,
* check if we need to cache generated policy files * check if we need to cache generated policy files
*/ */
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc) cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
/* Start file cache freshness checker */ /* Start file cache freshness checker */
go startFileMonitor(fileMonitorSleepTime) startFileMonitor(*fileMonitorFreq)
Config.LogSystem("File cache freshness monitor started with frequency: %s\n", fileMonitorSleepTime) Config.SysLog.Info("", "File caching enabled with: maxcount=%d maxsize=%.3fMB checkfreq=%s\n", *cacheSize, *cacheFileSizeMax, *fileMonitorFreq)
} else { } else {
/* File caching disabled, init with zero max size so nothing gets cached */ /* File caching disabled, init with zero max size so nothing gets cached */
Config.FileSystem.Init(2, 0) Config.FileSystem.Init(2, 0)
Config.LogSystem("File caching disabled\n") Config.SysLog.Info("", "File caching disabled\n")
/* Safe to cache policy files now */ /* Safe to cache policy files now */
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc) cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
} }
/* Setup file restrictions and remappings */
Config.FileSystem.Restricted = compileUserRestrictedRegex(*restrictedFiles)
Config.FileSystem.Remaps = compileUserRemapRegex(*fileRemaps)
/* Precompile some helpful regex */
Config.RgxGophermap = compileGophermapCheckRegex()
Config.RgxCgiBin = compileCgiBinCheckRegex()
/* Return the created listeners slice :) */ /* Return the created listeners slice :) */
return listeners return listeners
} }
@ -215,47 +236,6 @@ func setupServer() []*GophorListener {
func enterServerDir(path string) { func enterServerDir(path string) {
err := syscall.Chdir(path) err := syscall.Chdir(path)
if err != nil { if err != nil {
Config.LogSystemFatal("Error changing dir to server root %s: %s\n", path, err.Error()) log.Fatalf("Error changing dir to server root %s: %s\n", path, err.Error())
}
}
func chrootServerDir(path string) {
err := syscall.Chroot(path)
if err != nil {
Config.LogSystemFatal("Error chroot'ing into server root %s: %s\n", path, err.Error())
}
/* Change to server root just to ensure we're sitting at root of chroot */
err = syscall.Chdir("/")
if err != nil {
Config.LogSystemFatal("Error changing to root of chroot dir: %s\n", err.Error())
}
}
func setPrivileges(execUid, execGid int) {
/* Check root privileges aren't being requested */
if execUid == 0 || execGid == 0 {
Config.LogSystemFatal("Gophor does not support directly running as root\n")
}
/* Get currently running user info */
uid, gid := syscall.Getuid(), syscall.Getgid()
/* Set GID if necessary */
if gid != execUid {
/* C-bind setgid */
result := C.setgid(C.uint(execGid))
if result != 0 {
Config.LogSystemFatal("Failed setting GID %d: %d\n", execGid, result)
}
}
/* Set UID if necessary */
if uid != execGid {
/* C-bind setuid */
result := C.setuid(C.uint(execUid))
if result != 0 {
Config.LogSystemFatal("Failed setting UID %d: %d\n", execUid, result)
}
} }
} }

@ -1,5 +1,6 @@
package main package main
/* Function does, as function is named */
func generateHtmlRedirect(url string) []byte { func generateHtmlRedirect(url string) []byte {
content := content :=
"<html>\n"+ "<html>\n"+

@ -0,0 +1,209 @@
package main
import (
"io"
"bytes"
)
type HttpStripWriter struct {
/* Wrapper to io.Writer that reads a predetermined amount into a buffer
* then parses the buffer for valid HTTP headers and status code, deciding
* whether to strip these headers or returning with an HTTP status code.
*/
Writer io.Writer
SkipBuffer []byte
SkipIndex int
Err *GophorError
/* We set underlying write function with a variable, so that each call
* to .Write() doesn't have to perform a check every time whether we need
* to keep checking for headers to skip.
*/
WriteFunc func([]byte) (int, error)
}
func NewHttpStripWriter(writer io.Writer) *HttpStripWriter {
w := &HttpStripWriter{}
w.Writer = writer
w.WriteFunc = w.WriteCheckForHeaders
w.SkipBuffer = make([]byte, Config.SkipPrefixBufSize)
w.SkipIndex = 0
return w
}
func (w *HttpStripWriter) Size() int {
/* Size of the skip buffer */
return len(w.SkipBuffer)
}
func (w *HttpStripWriter) Available() int {
/* How much space have we got left in the skip buffer */
return w.Size() - w.SkipIndex
}
func (w *HttpStripWriter) AddToSkipBuffer(data []byte) int {
/* Figure out how much data we need to add */
toAdd := w.Available()
if len(data) < toAdd {
toAdd = len(data)
}
/* Add the data to the skip buffer! */
copy(w.SkipBuffer[w.SkipIndex:], data[:toAdd])
w.SkipIndex += toAdd
return toAdd
}
func (w *HttpStripWriter) ParseHttpHeaderSection() (bool, bool) {
/* Check if this is a valid HTTP header section and determine from status if we should continue */
validHeaderSection, shouldContinue := false, true
for _, header := range bytes.Split(w.SkipBuffer, []byte(DOSLineEnd)) {
header = bytes.ToLower(header)
if bytes.Contains(header, []byte("content-type: ")) {
/* This whole header section is now _valid_ */
validHeaderSection = true
} else if bytes.Contains(header, []byte("status: ")) {
/* Try parse status code */
statusStr := string(bytes.Split(bytes.TrimPrefix(header, []byte("status: ")), []byte(" "))[0])
if statusStr == "200" {
/* We ignore this */
continue
}
/* Any other values indicate error, we should not continue writing */
shouldContinue = false
/* Try parse error code */
errorCode := CgiStatusUnknownErr
switch statusStr {
case "400":
errorCode = CgiStatus400Err
case "401":
errorCode = CgiStatus401Err
case "403":
errorCode = CgiStatus403Err
case "404":
errorCode = CgiStatus404Err
case "408":
errorCode = CgiStatus408Err
case "410":
errorCode = CgiStatus410Err
case "500":
errorCode = CgiStatus500Err
case "501":
errorCode = CgiStatus501Err
case "503":
errorCode = CgiStatus503Err
}
/* Set struct error */
w.Err = &GophorError{ errorCode, nil }
}
}
return validHeaderSection, shouldContinue
}
func (w *HttpStripWriter) WriteSkipBuffer() (bool, error) {
defer func() {
w.SkipIndex = 0
}()
/* First try parse the headers, determine what to do next */
validHeaders, shouldContinue := w.ParseHttpHeaderSection()
if validHeaders {
/* Valid headers, we don't bother writing. Return whether
* shouldContinue whatever value that may be.
*/
return shouldContinue, nil
}
/* Default is to write skip buffer contents. shouldContinue only
* means something as long as we have valid headers.
*/
_, err := w.Writer.Write(w.SkipBuffer[:w.SkipIndex])
return true, err
}
func (w *HttpStripWriter) FinishUp() *GophorError {
/* If SkipBuffer still has contents, in case of data written being less
* than w.Size() --> check this data for HTTP headers to strip, parse
* any status codes and write this content with underlying writer if
* necessary.
*/
if w.SkipIndex > 0 {
w.WriteSkipBuffer()
}
/* Return HttpStripWriter error code if set */
return w.Err
}
func (w *HttpStripWriter) Write(data []byte) (int, error) {
/* Write using whatever write function is currently set */
return w.WriteFunc(data)
}
func (w *HttpStripWriter) WriteRegular(data []byte) (int, error) {
/* Regular write function */
return w.Writer.Write(data)
}
func (w *HttpStripWriter) WriteCheckForHeaders(data []byte) (int, error) {
split := bytes.Split(data, []byte(DOSLineEnd+DOSLineEnd))
if len(split) == 1 {
/* Try add these to skip buffer */
added := w.AddToSkipBuffer(data)
if added < len(data) {
defer func() {
/* Having written skipbuffer after this if clause, set write to regular */
w.WriteFunc = w.WriteRegular
}()
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
return len(data), io.EOF
} else if err != nil {
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
return added+count, err
}
}
return len(data), nil
} else {
defer func() {
/* No use for skip buffer after this clause, set write to regular */
w.WriteFunc = w.WriteRegular
w.SkipIndex = 0
}()
/* Try add what we can to skip buffer */
added := w.AddToSkipBuffer(append(split[0], []byte(DOSLineEnd+DOSLineEnd)...))
/* Write skip buffer data if necessary, check if we should continue */
doContinue, err := w.WriteSkipBuffer()
if !doContinue {
return len(data), io.EOF
} else if err != nil {
return added, err
}
/* Write remaining data not added to skip buffer */
count, err := w.Writer.Write(data[added:])
if err != nil {
return added+count, err
}
return len(data), nil
}
}

@ -3,69 +3,148 @@ package main
import ( import (
"log" "log"
"os" "os"
"io" "strings"
"io/ioutil"
) )
func setupLogging(loggingType int, systemLogPath, accessLogPath string) (*log.Logger, *log.Logger) { const (
/* Setup global logger */ /* Prefixes */
log.SetOutput(os.Stderr) LogPrefixInfo = ": I :: "
log.SetFlags(0) LogPrefixError = ": E :: "
LogPrefixFatal = ": F :: "
/* Log output types */
LogDisabled = "disable"
LogToStderr = "stderr"
LogToFile = "file"
/* Log options */
LogTimestamps = "timestamp"
LogIps = "ip"
)
/* Defines a simple logger interface */
type LoggerInterface interface {
Info(string, string, ...interface{})
Error(string, string, ...interface{})
Fatal(string, string, ...interface{})
}
/* Logger interface definition that does jack-shit */
type NullLogger struct {}
func (l *NullLogger) Info(prefix, format string, args ...interface{}) {}
func (l *NullLogger) Error(prefix, format string, args ...interface{}) {}
func (l *NullLogger) Fatal(prefix, format string, args ...interface{}) {}
/* A basic logger implemention */
type Logger struct {
Logger *log.Logger
}
func (l *Logger) Info(prefix, format string, args ...interface{}) {
l.Logger.Printf(LogPrefixInfo+prefix+format, args...)
}
func (l *Logger) Error(prefix, format string, args ...interface{}) {
l.Logger.Printf(LogPrefixError+prefix+format, args...)
}
func (l *Logger) Fatal(prefix, format string, args ...interface{}) {
l.Logger.Fatalf(LogPrefixFatal+prefix+format, args...)
}
/* Logger implementation that ignores the prefix (e.g. when not printing IPs) */
type LoggerNoPrefix struct {
Logger *log.Logger
}
func (l *LoggerNoPrefix) Info(prefix, format string, args ...interface{}) {
/* Ignore the prefix */
l.Logger.Printf(LogPrefixInfo+format, args...)
}
func (l *LoggerNoPrefix) Error(prefix, format string, args ...interface{}) {
/* Ignore the prefix */
l.Logger.Printf(LogPrefixError+format, args...)
}
func (l *LoggerNoPrefix) Fatal(prefix, format string, args ...interface{}) {
/* Ignore the prefix */
l.Logger.Fatalf(LogPrefixFatal+format, args...)
}
/* Setup the system and access logger interfaces according to supplied output options and logger options */
func setupLoggers(logOutput, logOpts, systemLogPath, accessLogPath string) (LoggerInterface, LoggerInterface) {
/* Parse the logger options */
logIps := false
logFlags := 0
for _, opt := range strings.Split(logOpts, ",") {
switch opt {
case "":
continue
case LogTimestamps:
logFlags = log.LstdFlags
case LogIps:
logIps = true
default:
log.Fatalf("Unrecognized log opt: %s\n")
}
}
/* Setup the loggers according to requested logging output */
switch logOutput {
case "":
/* Assume empty means stderr */
fallthrough
/* Calculate now, because, *shrug* */ case LogToStderr:
useSame := (systemLogPath == accessLogPath) /* Return two separate stderr loggers */
sysLogger := &LoggerNoPrefix{ NewLoggerToStderr(logFlags) }
/* Check requested logging type */ if logIps {
var systemLogger, accessLogger *log.Logger return sysLogger, &Logger{ NewLoggerToStderr(logFlags) }
switch loggingType {
case 0:
/* Default */
/* Setup system logger to output to file, or stderr if none supplied */
var systemWriter io.Writer
if systemLogPath != "" {
fd, err := os.OpenFile(systemLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalf("Failed to create system logger: %s\n", err.Error())
}
systemWriter = fd
} else { } else {
systemWriter = os.Stderr return sysLogger, &LoggerNoPrefix{ NewLoggerToStderr(logFlags) }
} }
systemLogger = log.New(systemWriter, "", log.LstdFlags)
/* If both output to same, may as well use same logger for both */ case LogDisabled:
if useSame { /* Return two pointers to same null logger */
accessLogger = systemLogger nullLogger := &NullLogger{}
} return nullLogger, nullLogger
/* Setup access logger to output to file, or stderr if none supplied */ case LogToFile:
var accessWriter io.Writer /* Return two separate file loggers */
if accessLogPath != "" { sysLogger := &Logger{ NewLoggerToFile(systemLogPath, logFlags) }
fd, err := os.OpenFile(accessLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if logIps {
if err != nil { return sysLogger, &Logger{ NewLoggerToFile(accessLogPath, logFlags) }
log.Fatalf("Failed to create access logger: %s\n", err.Error())
}
accessWriter = fd
} else { } else {
accessWriter = os.Stderr return sysLogger, &LoggerNoPrefix{ NewLoggerToFile(accessLogPath, logFlags) }
} }
accessLogger = log.New(accessWriter, "", log.LstdFlags)
case 1:
/* Disable -- pipe logs to "discard". May as well use same for both */
systemLogger = log.New(ioutil.Discard, "", 0)
accessLogger = systemLogger
default: default:
log.Fatalf("Unrecognized logging type: %d\n", loggingType) log.Fatalf("Unrecognised log output type: %s\n", logOutput)
return nil, nil
} }
return systemLogger, accessLogger
} }
/* Helper function to create new standard log.Logger to stderr */
func NewLoggerToStderr(logFlags int) *log.Logger {
return log.New(os.Stderr, "", logFlags)
}
/* Helper function to create new standard log.Logger to file */
func NewLoggerToFile(path string, logFlags int) *log.Logger {
writer, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalf("Failed to create logger to file %s: %s\n", path, err.Error())
}
return log.New(writer, "", logFlags)
}
/* Set the default logger flags before printing version */
func printVersionExit() { func printVersionExit() {
/* Reset the flags before printing version */
log.SetFlags(0) log.SetFlags(0)
log.Printf("%s\n", GophorVersion) log.Printf("%s\n", GophorVersion)
os.Exit(0) os.Exit(0)

@ -0,0 +1,154 @@
package main
import (
"strings"
"net/url"
)
/* Parse a request string into a path and parameters string */
func parseGopherUrl(request string) (*GopherUrl, *GophorError) {
if strings.Contains(request, "#") || // we don't support fragments
strings.HasPrefix(request, "GET ") { // we don't support HTTP requests
return nil, &GophorError{ InvalidRequestErr, nil }
}
/* Check if string contains any ASCII control byte */
for i := 0; i < len(request); i += 1 {
if request[i] < ' ' || request[i] == 0x7f {
return nil, &GophorError{ InvalidRequestErr, nil }
}
}
/* Split into 2 substrings by '?'. Url path and query */
split := strings.SplitN(request, "?", 2)
/* Unescape path */
path, err := url.PathUnescape(split[0])
if err != nil {
return nil, &GophorError{ InvalidRequestErr, nil }
}
/* Return GopherUrl based on this split request */
if len(split) == 1 {
return &GopherUrl{ path, "" }, nil
} else {
return &GopherUrl{ path, split[1] }, nil
}
}
/* Parse line type from contents */
func parseLineType(line string) ItemType {
lineLen := len(line)
if lineLen == 0 {
return TypeInfoNotStated
} else if lineLen == 1 {
/* The only accepted types for a length 1 line */
switch ItemType(line[0]) {
case TypeEnd:
return TypeEnd
case TypeEndBeginList:
return TypeEndBeginList
case TypeComment:
return TypeComment
case TypeInfo:
return TypeInfo
case TypeTitle:
return TypeTitle
default:
return TypeUnknown
}
} else if !strings.Contains(line, string(Tab)) {
/* The only accepted types for a line with no tabs */
switch ItemType(line[0]) {
case TypeComment:
return TypeComment
case TypeTitle:
return TypeTitle
case TypeInfo:
return TypeInfo
case TypeHiddenFile:
return TypeHiddenFile
case TypeSubGophermap:
return TypeSubGophermap
default:
return TypeInfoNotStated
}
}
return ItemType(line[0])
}
/* Parses a line in a gophermap into a new request object */
func parseLineRequestString(requestPath *RequestPath, lineStr string) (*Request, *GophorError) {
if strings.HasPrefix(lineStr, "/") {
/* Assume is absolute (well, seeing server root as '/') */
if withinCgiBin(lineStr[1:]) {
/* CGI script, parse request path and parameters */
url, gophorErr := parseGopherUrl(lineStr[1:])
if gophorErr != nil {
return nil, gophorErr
} else {
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
}
} else {
/* Regular file, no more parsing */
return &Request{ NewRequestPath(requestPath.RootDir(), lineStr[1:]), "" }, nil
}
} else {
/* Assume relative to current directory */
if withinCgiBin(lineStr) && requestPath.Relative() == "" {
/* If begins with cgi-bin and is at root dir, parse as cgi-bin */
url, gophorErr := parseGopherUrl(lineStr)
if gophorErr != nil {
return nil, gophorErr
} else {
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
}
} else {
/* Regular file, no more parsing */
return &Request{ NewRequestPath(requestPath.RootDir(), requestPath.JoinCurDir(lineStr)), "" }, nil
}
}
}
/* Split a string according to a rune, that supports delimiting with '\' */
func splitStringByRune(str string, r rune) []string {
ret := make([]string, 0)
buf := ""
delim := false
for _, c := range str {
switch c {
case r:
if !delim {
ret = append(ret, buf)
buf = ""
} else {
buf += string(c)
delim = false
}
case '\\':
if !delim {
delim = true
} else {
buf += string(c)
delim = false
}
default:
if !delim {
buf += string(c)
} else {
buf += "\\"+string(c)
delim = false
}
}
}
if len(buf) > 0 || len(ret) == 0 {
ret = append(ret, buf)
}
return ret
}

@ -2,53 +2,66 @@ package main
import ( import (
"os" "os"
"path"
"sync"
) )
func cachePolicyFiles(description, admin, geoloc string) { const (
/* Filename constants */
CapsTxtStr = "caps.txt"
RobotsTxtStr = "robots.txt"
)
func cachePolicyFiles(rootDir, description, admin, geoloc string) {
/* See if caps txt exists, if not generate */ /* See if caps txt exists, if not generate */
_, err := os.Stat("/caps.txt") _, err := os.Stat(path.Join(rootDir, CapsTxtStr))
if err != nil { if err != nil {
/* We need to generate the caps txt and manually load into cache */ /* We need to generate the caps txt and manually load into cache */
content := generateCapsTxt(description, admin, geoloc) content := generateCapsTxt(description, admin, geoloc)
/* Create new file object from generated file contents */ /* Create new file object from generated file contents */
fileContents := &GeneratedFileContents{ content } fileContents := &GeneratedFileContents{ content }
file := NewFile(fileContents) file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */ /* Trigger a load contents just to set it as fresh etc */
file.LoadContents() file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */ /* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/caps.txt", file) Config.FileSystem.CacheMap.Put(rootDir+"/"+CapsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+CapsTxtStr)
Config.LogSystem("Generated policy file: /caps.txt\n")
} }
/* See if caps txt exists, if not generate */ /* See if robots txt exists, if not generate */
_, err = os.Stat("/robots.txt") _, err = os.Stat(rootDir+"/"+RobotsTxtStr)
if err != nil { if err != nil {
/* We need to generate the caps txt and manually load into cache */ /* We need to generate the robots txt and manually load into cache */
content := generateRobotsTxt() content := generateRobotsTxt()
/* Create new file object from generated file contents */ /* Create new file object from generated file contents */
fileContents := &GeneratedFileContents{ content } fileContents := &GeneratedFileContents{ content }
file := NewFile(fileContents) file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */ /* Trigger a load contents just to set it as fresh etc */
file.LoadContents() file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */ /* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/robots.txt", file) Config.FileSystem.CacheMap.Put(rootDir+"/"+RobotsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+RobotsTxtStr)
Config.LogSystem("Generated policy file: /robots.txt\n")
} }
} }
func generatePolicyHeader(filename string) string {
text := "# This is an automatically generated"+DOSLineEnd
text += "# server policy file: "+filename+DOSLineEnd
text += "#"+DOSLineEnd
text += "# Eat the rich ~GophorDev"+DOSLineEnd
return text
}
func generateCapsTxt(description, admin, geoloc string) []byte { func generateCapsTxt(description, admin, geoloc string) []byte {
text := "CAPS"+DOSLineEnd text := "CAPS"+DOSLineEnd
text += DOSLineEnd text += DOSLineEnd
text += "# This is an automatically generated"+DOSLineEnd text += generatePolicyHeader(CapsTxtStr)
text += "# server policy file: caps.txt"+DOSLineEnd
text += DOSLineEnd text += DOSLineEnd
text += "CapsVersion=1"+DOSLineEnd text += "CapsVersion=1"+DOSLineEnd
text += "ExpireCapsAfter=1800"+DOSLineEnd text += "ExpireCapsAfter=1800"+DOSLineEnd
@ -64,14 +77,16 @@ func generateCapsTxt(description, admin, geoloc string) []byte {
text += "ServerSoftwareVersion="+GophorVersion+DOSLineEnd text += "ServerSoftwareVersion="+GophorVersion+DOSLineEnd
text += "ServerDescription="+description+DOSLineEnd text += "ServerDescription="+description+DOSLineEnd
text += "ServerGeolocationString="+geoloc+DOSLineEnd text += "ServerGeolocationString="+geoloc+DOSLineEnd
text += "ServerDefaultEncoding=ascii"+DOSLineEnd // text += "ServerDefaultEncoding=ascii"+DOSLineEnd
text += DOSLineEnd text += DOSLineEnd
text += "ServerAdmin="+admin+DOSLineEnd text += "ServerAdmin="+admin+DOSLineEnd
return []byte(text) return []byte(text)
} }
func generateRobotsTxt() []byte { func generateRobotsTxt() []byte {
text := "Usage-agent: *"+DOSLineEnd text := generatePolicyHeader(RobotsTxtStr)
text += DOSLineEnd
text += "Usage-agent: *"+DOSLineEnd
text += "Disallow: *"+DOSLineEnd text += "Disallow: *"+DOSLineEnd
text += DOSLineEnd text += DOSLineEnd
text += "Crawl-delay: 99999"+DOSLineEnd text += "Crawl-delay: 99999"+DOSLineEnd

@ -3,32 +3,92 @@ package main
import ( import (
"regexp" "regexp"
"strings" "strings"
"log"
) )
func compileUserRestrictedFilesRegex(restrictedFiles string) []*regexp.Regexp { const (
Config.LogSystem("Compiling restricted file regular expressions\n") FileRemapSeparatorStr = " -> "
)
type FileRemap struct {
Regex *regexp.Regexp
Template string
}
/* Pre-compile gophermap file string regex */
func compileGophermapCheckRegex() *regexp.Regexp {
return regexp.MustCompile(`^(|.+/|.+\.)gophermap$`)
}
/* Pre-compile cgi-bin path string regex */
func compileCgiBinCheckRegex() *regexp.Regexp {
return regexp.MustCompile(`^cgi-bin(|/.*)$`)
}
/* Compile a user supplied new line separated list of regex statements */
func compileUserRestrictedRegex(restrictions string) []*regexp.Regexp {
/* Return slice */ /* Return slice */
restrictedFilesRegex := make([]*regexp.Regexp, 0) restrictedRegex := make([]*regexp.Regexp, 0)
/* Split the user supplied regex statements by new line */
for _, expr := range strings.Split(restrictions, "\n") {
/* Empty expression, skip */
if len(expr) == 0 {
continue
}
/* Split the user supplied RestrictedFiles string by new-line */ /* Try compile regex */
for _, expr := range strings.Split(restrictedFiles, "\n") {
regex, err := regexp.Compile(expr) regex, err := regexp.Compile(expr)
if err != nil { if err != nil {
Config.LogSystemFatal("Failed compiling user restricted files regex: %s\n", expr) log.Fatalf("Failed compiling user supplied regex: %s\n", expr)
} }
restrictedFilesRegex = append(restrictedFilesRegex, regex)
/* Append restricted */
restrictedRegex = append(restrictedRegex, regex)
Config.SysLog.Info("", "Compiled restricted: %s\n", expr)
} }
return restrictedFilesRegex return restrictedRegex
} }
/* Iterate through restricted file expressions, check if file _is_ restricted */ /* Compile a user supplied new line separated list of file remap regex statements */
func isRestrictedFile(name string) bool { func compileUserRemapRegex(remaps string) []*FileRemap {
for _, regex := range Config.RestrictedFiles { /* Return slice */
if regex.MatchString(name) { fileRemaps := make([]*FileRemap, 0)
return true
/* Split the user supplied regex statements by new line */
for _, expr := range strings.Split(remaps, "\n") {
/* Empty expression, skip */
if len(expr) == 0 {
continue
} }
/* Split into alias and remap string (MUST BE LENGTH 2) */
split := strings.Split(expr, FileRemapSeparatorStr)
if len(split) != 2 {
continue
}
/* Try compile regex */
regex, err := regexp.Compile("(?m)"+strings.TrimPrefix(split[0], "/")+"$")
if err != nil {
log.Fatalf("Failed compiling user supplied regex: %s\n", expr)
}
/* Append file remapper */
fileRemaps = append(fileRemaps, &FileRemap{ regex, strings.TrimPrefix(split[1], "/") })
Config.SysLog.Info("", "Compiled remap: %s\n", expr)
} }
return false
return fileRemaps
}
/* Check if file path is gophermap */
func isGophermap(path string) bool {
return Config.RgxGophermap.MatchString(path)
}
/* Check if file path within cgi-bin */
func withinCgiBin(path string) bool {
return Config.RgxCgiBin.MatchString(path)
} }

@ -0,0 +1,130 @@
package main
import (
"path"
"strings"
)
type RequestPath struct {
/* Path structure to allow hosts at
* different roots while maintaining relative
* and absolute path names for filesystem reading
*/
Root string
Rel string
Abs string
Select string
}
func NewRequestPath(rootDir, relPath string) *RequestPath {
return &RequestPath{ rootDir, relPath, path.Join(rootDir, strings.TrimSuffix(relPath, "/")), relPath }
}
func (rp *RequestPath) RemapPath(newPath string) *RequestPath {
requestPath := NewRequestPath(rp.RootDir(), sanitizeRawPath(rp.RootDir(), newPath))
requestPath.Select = rp.Relative()
return requestPath
}
func (rp *RequestPath) RootDir() string {
return rp.Root
}
func (rp *RequestPath) Relative() string {
return rp.Rel
}
func (rp *RequestPath) Absolute() string {
return rp.Abs
}
func (rp *RequestPath) Selector() string {
if rp.Select == "." {
return "/"
} else {
return "/"+rp.Select
}
}
func (rp *RequestPath) JoinRel(extPath string) string {
return path.Join(rp.Relative(), extPath)
}
func (rp *RequestPath) JoinAbs(extPath string) string {
return path.Join(rp.Absolute(), extPath)
}
func (rp *RequestPath) JoinSelector(extPath string) string {
return path.Join(rp.Selector(), extPath)
}
func (rp *RequestPath) HasAbsPrefix(prefix string) bool {
return strings.HasPrefix(rp.Absolute(), prefix)
}
func (rp *RequestPath) HasRelPrefix(prefix string) bool {
return strings.HasPrefix(rp.Relative(), prefix)
}
func (rp *RequestPath) HasRelSuffix(suffix string) bool {
return strings.HasSuffix(rp.Relative(), suffix)
}
func (rp *RequestPath) HasAbsSuffix(suffix string) bool {
return strings.HasSuffix(rp.Absolute(), suffix)
}
func (rp *RequestPath) TrimRelSuffix(suffix string) string {
return strings.TrimSuffix(strings.TrimSuffix(rp.Relative(), suffix), "/")
}
func (rp *RequestPath) TrimAbsSuffix(suffix string) string {
return strings.TrimSuffix(strings.TrimSuffix(rp.Absolute(), suffix), "/")
}
func (rp *RequestPath) JoinCurDir(extPath string) string {
return path.Join(path.Dir(rp.Relative()), extPath)
}
func (rp *RequestPath) JoinRootDir(extPath string) string {
return path.Join(rp.RootDir(), extPath)
}
type Request struct {
/* Holds onto a request path to the filesystem and
* a string slice of parsed parameters (usually nil
* or length 1)
*/
Path *RequestPath
Parameters string
}
func NewSanitizedRequest(rootDir string, url *GopherUrl) *Request {
return &Request{
NewRequestPath(
rootDir,
sanitizeRawPath(rootDir, url.Path),
),
url.Parameters,
}
}
/* Sanitize a request path string */
func sanitizeRawPath(rootDir, relPath string) string {
/* Start with a clean :) */
relPath = path.Clean(relPath)
if path.IsAbs(relPath) {
/* Is absolute. Try trimming root and leading '/' */
relPath = strings.TrimPrefix(strings.TrimPrefix(relPath, rootDir), "/")
} else {
/* Is relative. If back dir traversal, give them root */
if strings.HasPrefix(relPath, "..") {
relPath = ""
}
}
return relPath
}

@ -0,0 +1,54 @@
package main
import (
"io"
)
type Responder struct {
Conn *BufferedDeadlineConn
Host *ConnHost
Client *ConnClient
Request *Request
}
func NewResponder(conn *BufferedDeadlineConn, host *ConnHost, client *ConnClient, request *Request) *Responder {
return &Responder{ conn, host, client, request }
}
func (r *Responder) AccessLogInfo(format string, args ...interface{}) {
Config.AccLog.Info("("+r.Client.Ip()+") ", format, args...)
}
func (r *Responder) AccessLogError(format string, args ...interface{}) {
Config.AccLog.Error("("+r.Client.Ip()+") ", format, args...)
}
func (r *Responder) Write(b []byte) (int, error) {
return r.Conn.Write(b)
}
func (r *Responder) WriteData(data []byte) *GophorError {
err := r.Conn.WriteData(data)
if err != nil {
return &GophorError{ SocketWriteErr, err }
}
return nil
}
func (r *Responder) WriteRaw(reader io.Reader) *GophorError {
err := r.Conn.WriteRaw(reader)
if err != nil {
return &GophorError{ SocketWriteRawErr, err }
}
return nil
}
func (r *Responder) CloneWithRequest(request *Request) *Responder {
/* Create new copy of Responder only with request differring */
return &Responder{
r.Conn,
r.Host,
r.Client,
request,
}
}

@ -1,155 +1,71 @@
package main package main
import ( import (
"path"
"strings" "strings"
) )
type Worker struct { type Worker struct {
Conn *GophorConn Conn *BufferedDeadlineConn
} Host *ConnHost
Client *ConnClient
func NewWorker(conn *GophorConn) *Worker { RootDir string
return &Worker{ conn }
} }
func (worker *Worker) Serve() { func (worker *Worker) Serve() {
defer func() { defer worker.Conn.Close()
/* Close-up shop */
worker.Conn.Close()
}()
var count int
var err error
/* Read buffer + final result */
buf := make([]byte, SocketReadBufSize)
received := make([]byte, 0)
iter := 0
for {
/* Buffered read from listener */
count, err = worker.Conn.Read(buf)
if err != nil {
Config.LogSystemError("Error reading from socket on port %s: %s\n", worker.Conn.Host.Port, err.Error())
return
}
/* Only copy non-null bytes */
received = append(received, buf[:count]...)
/* If count is less than expected read size, we've hit EOF */
if count < SocketReadBufSize {
/* EOF */
break
}
/* Hit max read chunk size, send error + close connection */
if iter == MaxSocketReadChunks {
Config.LogSystemError("Reached max socket read size %d. Closing connection...\n", MaxSocketReadChunks*SocketReadBufSize)
return
}
/* Keep count :) */
iter += 1
}
/* Handle request */
gophorErr := worker.RespondGopher(received)
/* Handle any error */
if gophorErr != nil {
Config.LogSystemError("%s\n", gophorErr.Error())
/* Generate response bytes from error code */ line, err := worker.Conn.ReadLine()
response := generateGopherErrorResponseFromCode(gophorErr.Code)
/* If we got response bytes to send? SEND 'EM! */
if response != nil {
/* No gods. No masters. We don't care about error checking here */
worker.SendRaw(response)
}
}
}
func (worker *Worker) SendRaw(b []byte) *GophorError {
count, err := worker.Conn.Write(b)
if err != nil { if err != nil {
return &GophorError{ SocketWriteErr, err } Config.SysLog.Error("", "Error reading from socket port %s: %s\n", worker.Host.Port(), err.Error())
} else if count != len(b) { return
return &GophorError{ SocketWriteCountErr, nil }
} }
return nil
}
func (worker *Worker) Log(format string, args ...interface{}) {
Config.LogAccess(worker.Conn.RemoteAddr().String(), format, args...)
}
func (worker *Worker) LogError(format string, args ...interface{}) { /* Drop up to first tab */
Config.LogAccessError(worker.Conn.RemoteAddr().String(), format, args...) received := strings.Split(string(line), Tab)[0]
}
func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* According to Gopher spec, only read up to first Tab or Crlf */
dataStr := readUpToFirstTabOrCrlf(data)
/* Handle URL request if presented */ /* Handle URL request if presented */
lenBefore := len(dataStr) lenBefore := len(received)
dataStr = strings.TrimPrefix(dataStr, "URL:") received = strings.TrimPrefix(received, "URL:")
switch len(dataStr) { switch len(received) {
case lenBefore-4: case lenBefore-4:
/* Send an HTML redirect to supplied URL */ /* Send an HTML redirect to supplied URL */
worker.Log("Redirecting to %s\n", dataStr) Config.AccLog.Info("("+worker.Client.Ip()+") ", "Redirecting to %s\n", received)
return worker.SendRaw(generateHtmlRedirect(dataStr)) worker.Conn.Write(generateHtmlRedirect(received))
return
default: default:
/* Do nothing */ /* Do nothing */
} }
/* Sanitize supplied path */ /* Create GopherUrl object from request string */
requestPath := sanitizePath(dataStr) url, gophorErr := parseGopherUrl(received)
if gophorErr == nil {
/* Append lastline */ /* Create new request from url object */
response, gophorErr := Config.FileSystem.HandleRequest(requestPath, worker.Conn.Host) request := NewSanitizedRequest(worker.RootDir, url)
if gophorErr != nil {
worker.LogError("Failed to serve: %s\n", requestPath)
return gophorErr
}
worker.Log("Served: %s\n", requestPath)
/* Serve response */ /* Create new responder from request */
return worker.SendRaw(response) responder := NewResponder(worker.Conn, worker.Host, worker.Client, request)
}
func readUpToFirstTabOrCrlf(data []byte) string { /* Handle request with supplied responder */
/* Only read up to first tab or cr-lf */ gophorErr = Config.FileSystem.HandleRequest(responder)
dataStr := "" if gophorErr == nil {
dataLen := len(data) /* Log success to access and return! */
for i := 0; i < dataLen; i += 1 { responder.AccessLogInfo("Served: %s\n", request.Path.Absolute())
switch data[i] { return
case '\t': } else {
return dataStr /* Log failure to access */
case DOSLineEnd[0]: responder.AccessLogError("Failed to serve: %s\n", request.Path.Absolute())
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
return dataStr
}
default:
dataStr += string(data[i])
} }
} }
return dataStr /* Log serve failure to error to system */
} Config.SysLog.Error("", gophorErr.Error())
func sanitizePath(dataStr string) string { /* Generate response bytes from error code */
/* Clean path and trim '/' prefix if still exists */ errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
requestPath := strings.TrimPrefix(path.Clean(dataStr), "/")
if requestPath == "." { /* If we got response bytes to send? SEND 'EM! */
requestPath = "/" if errResponse != nil {
} else if !strings.HasPrefix(requestPath, "/") { /* No gods. No masters. We don't care about error checking here */
requestPath = "/" + requestPath worker.Conn.WriteData(errResponse)
} }
return requestPath
} }

Loading…
Cancel
Save