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
- Built with concurrency and efficiency in mind.
- Built with security, concurrency and efficiency in mind.
- ZERO external dependencies.
- Security focused -- chroots into server direrctory and drops
privileges. `maybe wait until stable release before use outside of hobby
setups.`
- LRU file caching -- with user-controlled cache size, max cached file size
- LRU file caching with user-controlled cache size, max cached file size
and cache refresh frequency.
- Insert files within gophermaps, including automating reflowing of lines
longer than (user definable) page width.
- CGI/1.1 support (see below for CGI environment variables set).
- Automatic replacement of `$hostname` or `$port` with the information of
the host the client is connecting to.
- URL encoding with query support.
- 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
disable both).
- Executable gophermap support.
# Usage
- Insert files with automated line reflowing, output of any CGI scripts
or executable gophermaps WITHIN gophermaps.
```
gophor [args]
-root Change server root directory.
- Support for all commonly accepted item type characters (beyond just
RFC1436 support).
- 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
lists).
`/virtual/file -> /actual/file`
-bind-addr Change server bind-address (used in creating
socket).
e.g. scripts within `cgi-bin` to the root directory:
`/(?P<script>[^/]+) -> /cgi-bin/$script`
-user Drop to supplied user's UID and GID permissions
before execution.
Entries are parsed, compiled, and so matched-against in order.
-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
separated lines).
There is a near 10 year ongoing tracked issue
(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
restricting files from showing in directory listing.
If you run into issues binding to a lower port number due to insufficient
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
@ -100,6 +167,7 @@ text lines before sending to connecting clients.
```
RFC 1436 Standard:
Type | Treat as | Meaning
--------------------------
0 | TEXT | Regular file (text)
1 | MENU | Directory (menu)
2 | EXTERNAL | CCSO flat db; other db
@ -117,6 +185,7 @@ Type | Treat as | Meaning
GopherII Standard:
Type | Treat as | Meaning
--------------------------
c | BINARY | Calendar file
d | BINARY | Word-processing document; PDF document
h | TEXT | HTML document
@ -129,22 +198,28 @@ Type | Treat as | Meaning
Commonly used:
Type | Treat as | Meaning
--------------------------
. | - | Last line -- stop processing gophermap default
! | - | [SERVER ONLY] Menu title (set title ONCE per gophermap)
# | - | [SERVER ONLY] Comment, rest of line is ignored
- | - | [SERVER ONLY] Hide file/directory from directory listing
. | - | [SERVER ONLY] Last line -- stop processing gophermap default
* | - | [SERVER ONLY] Last line + directory listing -- stop processing
| | gophermap and end on a directory listing
= | - | [SERVER ONLY] Include subgophermap / regular file here. Prints
| | and formats file / gophermap in-place
Planned to be supported:
Type | Treat as | Meaning
$ | - | [SERVER ONLY] Execute shell command and print stdout here
= | - | [SERVER ONLY] Include or execute subgophermap, cgi-bin or regular
| | file 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
We aim to comply more with GopherII (see in references below).
## Item types
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`.
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
Upon request, `caps.txt` can be provided from the server root directory
@ -195,9 +320,9 @@ Possible Gophor errors:
## Terminating full stop
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...
@ -209,59 +334,29 @@ Null port: `0`
# Todos
Shortterm:
- 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?
- Support setting character encoding
- Add last-mod-time to directory listings -- have global time parser
object, maybe separate out separate global instances of objects (e.g.
worker related, cache related, config related?)
- improve organization of what logs go where (e.g. to sys or acc)
- TLS support -- ~~requires a rethink of how we're passing port functions
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.
- Move filesystem_read functions to FileSystem struct function
- Connection throttling + timeouts -- thread to keep track of list of
recently connected IPs. Keep incremementing connection count and only
remove from list when `lastIncremented` time is greater than timeout
- FastCGI support
- More closely follow GoLang built-in net/http code style for worker -- just
a neatness thing, maybe bring some performance improvements too and a
generally different way of approaching some of the solutions to problems we
have
- Personal user gopherspaces
# Please note
- Rotating logs
During the initial writing phase the quality of git commit messages may be
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.
- TLS support
As soon as we reach a stable point in development, or if other people start
contributing issues or PRs, whichever comes first, this will be changed
right away.
- Connection throttling + timeouts
# Resources used
Gopher-II (The Next Generation Gopher WWIS):
https://tools.ietf.org/html/draft-matavka-gopher-ii-00
Gophernicus supported item types:
https://github.com/gophernicus/gophernicus/blob/master/README.gophermap
Gophernicus source (a great gopher daemon in C):
https://github.com/gophernicus/gophernicus
All of the below can be viewed from your standard web browser using
floodgap's Gopher proxy:

@ -3,13 +3,13 @@
set -e
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.*$||')"
LOGFILE='build.log'
OUTDIR="build-${VERSION}"
silent() {
"$@" > "$LOGFILE" 2>&1
"$@" >> "$LOGFILE" 2>&1
}
build_for() {
@ -22,15 +22,31 @@ build_for() {
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" "$@"
CGO_ENABLED=1 CC="$toolchain-gcc" 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 "Attempting to compress ${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 ""
}
@ -39,33 +55,97 @@ echo "YOUR CC TOOLCHAIN LOCATIONS MAY DIFFER"
echo "IF THE SCRIPT FAILS, CHECK THE OUTPUT OF: ${LOGFILE}"
echo ""
# Clean logfile
rm -f "$LOGFILE"
# Clean and recreate directory
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
# 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
import (
"time"
"regexp"
"log"
)
/* ServerConfig:
@ -12,38 +12,35 @@ import (
* and file cache)
*/
type ServerConfig struct {
/* Base settings */
RootDir string
/* Executable Settings */
Env []string
CgiEnv []string
CgiEnabled bool
MaxExecRunTime time.Duration
/* Content settings */
FooterText []byte
PageWidth int
RestrictedFiles []*regexp.Regexp
FooterText []byte
PageWidth int
/* Logging */
SystemLogger *log.Logger
AccessLogger *log.Logger
SysLog LoggerInterface
AccLog LoggerInterface
/* Filesystem access */
FileSystem *FileSystem
}
func (config *ServerConfig) LogSystem(fmt string, args ...interface{}) {
config.SystemLogger.Printf(":: I :: "+fmt, args...)
}
func (config *ServerConfig) LogSystemError(fmt string, args ...interface{}) {
config.SystemLogger.Printf(":: E :: "+fmt, args...)
}
func (config *ServerConfig) LogSystemFatal(fmt string, args ...interface{}) {
config.SystemLogger.Fatalf(":: F :: "+fmt, args...)
}
func (config *ServerConfig) LogAccess(sourceAddr, fmt string, args ...interface{}) {
config.AccessLogger.Printf(":: I :: ["+sourceAddr+"] "+fmt, args...)
}
func (config *ServerConfig) LogAccessError(sourceAddr, fmt string, args ...interface{}) {
config.AccessLogger.Printf(":: E :: ["+sourceAddr+"] "+fmt, args...)
FileSystem *FileSystem
/* Buffer sizes */
SocketWriteBufSize int
SocketReadBufSize int
SocketReadMax int
SkipPrefixBufSize int
FileReadBufSize int
/* Socket deadlines */
SocketReadDeadline time.Duration
SocketWriteDeadline time.Duration
/* Precompiled regular expressions */
RgxGophermap *regexp.Regexp
RgxCgiBin *regexp.Regexp
}

@ -1,27 +1,64 @@
package main
import (
"io"
"net"
"time"
"bufio"
"strconv"
)
/* Data structure to hold specific host details */
type ConnHost struct {
Name string
Port string
/* Hold host specific details */
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 {
/* Simple net.Listener wrapper that holds onto virtual
* host information + generates Worker instances on Accept()
*/
Listener net.Listener
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.Host = &ConnHost{ hostname, port }
gophorListener.Host = &ConnHost{ hostname, port, fwdPort }
gophorListener.Root = rootDir
var err error
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()
if err != nil {
return nil, err
}
gophorConn := new(GophorConn)
gophorConn.Conn = conn
gophorConn.Host = &ConnHost{ l.Host.Name, l.Host.Port }
return gophorConn, nil
/* Should always be ok as listener is type TCP (see above) */
addr, _ := conn.RemoteAddr().(*net.TCPAddr)
client := &ConnClient{ addr.IP.String(), strconv.Itoa(addr.Port) }
return &Worker{ NewBufferedDeadlineConn(conn), l.Host, client, l.Root }, nil
}
func (l *GophorListener) Addr() net.Addr {
return l.Listener.Addr()
type DeadlineConn struct {
/* Simple wrapper to net.Conn that sets deadlines
* on each call to Read() / Write()
*/
conn net.Conn
}
/* Simple wrapper to Conn with easier acccess
* to hostname / port information
*/
type GophorConn struct {
Conn net.Conn
Host *ConnHost
func NewDeadlineConn(conn net.Conn) *DeadlineConn {
return &DeadlineConn{ conn }
}
func (c *DeadlineConn) Read(b []byte) (int, error) {
/* 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) {
return c.Conn.Read(b)
func (c *BufferedDeadlineConn) Write(b []byte) (int, error) {
return c.buffer.Write(b)
}
func (c *GophorConn) Write(b []byte) (int, error) {
return c.Conn.Write(b)
func (c *BufferedDeadlineConn) WriteData(b []byte) error {
_, err := c.buffer.Write(b)
return err
}
func (c *GophorConn) RemoteAddr() net.Addr {
return c.Conn.RemoteAddr()
func (c *BufferedDeadlineConn) WriteRaw(r io.Reader) error {
_, err := c.buffer.ReadFrom(r)
return err
}
func (c *GophorConn) Close() error {
return c.Conn.Close()
func (c *BufferedDeadlineConn) Close() error {
/* 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
const (
/* Filesystem */
PathEnumerationErr ErrorCode = iota
IllegalPathErr ErrorCode = iota
FileStatErr ErrorCode = iota
FileOpenErr ErrorCode = iota
FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota
PathEnumerationErr ErrorCode = iota
IllegalPathErr ErrorCode = iota
FileStatErr ErrorCode = iota
FileOpenErr ErrorCode = iota
FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota
/* Sockets */
SocketWriteErr ErrorCode = iota
SocketWriteCountErr ErrorCode = iota
SocketWriteErr ErrorCode = iota
SocketWriteRawErr ErrorCode = iota
/* Parsing */
InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr ErrorCode = iota
EntityPortParseErr ErrorCode = iota
InvalidGophermapErr ErrorCode = iota
InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr 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 */
ErrorResponse200 ErrorResponseCode = iota
@ -67,19 +85,47 @@ func (e *GophorError) Error() string {
str = "directory read fail"
case SocketWriteErr:
str = "socket write fail"
case SocketWriteCountErr:
str = "socket write count mismatch"
str = "socket write error"
case SocketWriteRawErr:
str = "socket write readFrom error"
case InvalidRequestErr:
str = "invalid request data"
case EmptyItemTypeErr:
str = "line string provides no dir entity type"
case EntityPortParseErr:
str = "parsing dir entity port"
case InvalidGophermapErr:
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:
str = "Unknown"
}
@ -113,16 +159,44 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
/* These are errors _while_ sending, no point trying to send error */
case SocketWriteErr:
return NoResponse
case SocketWriteCountErr:
case SocketWriteRawErr:
return NoResponse
case InvalidRequestErr:
return ErrorResponse400
case EmptyItemTypeErr:
case InvalidGophermapErr:
return ErrorResponse500
case EntityPortParseErr:
case CommandStartErr:
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
default:
@ -141,14 +215,16 @@ func generateGopherErrorResponseFromCode(code ErrorCode) []byte {
/* Generates gopher protocol compatible error response for response code */
func generateGopherErrorResponse(code ErrorResponseCode) []byte {
return buildError(code.String())
return buildErrorLine(code.String())
}
/* Error response code to string */
func (e ErrorResponseCode) String() string {
switch e {
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:
return "400 Bad Request"
case ErrorResponse401:
@ -169,7 +245,7 @@ func (e ErrorResponseCode) String() string {
return "503 Service Unavailable"
default:
/* Should not have reached here */
Config.LogSystemFatal("Unhandled ErrorResponseCode type\n")
Config.SysLog.Fatal("", "Unhandled ErrorResponseCode type\n")
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 (
"bytes"
"bufio"
"strings"
"os"
)
/* GeneratedFileContents:
* The simplest implementation of FileContents that
* stores some bytes and does nothing else.
*/
type FileContents interface {
/* Interface that provides an adaptable implementation
* for holding onto some level of information about the
* contents of a file.
*/
Render(*Responder) *GophorError
Load() *GophorError
Clear()
}
type GeneratedFileContents struct {
contents []byte
/* Super simple, holds onto a slice of bytes */
Contents []byte
}
func (fc *GeneratedFileContents) Render(request *FileSystemRequest) []byte {
return fc.contents
func (fc *GeneratedFileContents) Render(responder *Responder) *GophorError {
return responder.WriteData(fc.Contents)
}
func (fc *GeneratedFileContents) Load() *GophorError {
@ -27,141 +35,174 @@ func (fc *GeneratedFileContents) Clear() {
/* 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 {
path string
contents []byte
/* Simple implemention that holds onto a RequestPath
* and slice containing cache'd content
*/
Path *RequestPath
Contents []byte
}
func (fc *RegularFileContents) Render(request *FileSystemRequest) []byte {
/* Here we can ignore the extra data in request.
* We are but a simple cache'd file
*/
return fc.contents
func (fc *RegularFileContents) Render(responder *Responder) *GophorError {
return responder.WriteData(fc.Contents)
}
func (fc *RegularFileContents) Load() *GophorError {
/* Load the file into memory */
var gophorErr *GophorError
fc.contents, gophorErr = bufferedRead(fc.path)
fc.Contents, gophorErr = bufferedRead(fc.Path.Absolute())
return gophorErr
}
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 {
path string
sections []GophermapSection
}
/* Holds onto a RequestPath and slice containing individually
* renderable sections of the gophermap.
*/
func (gc *GophermapContents) Render(request *FileSystemRequest) []byte {
returnContents := make([]byte, 0)
Request *Request
Sections []GophermapSection
}
/* We don't just want to read the contents, each section
* in the sections slice needs a call to render() to
* perform their own required actions in producing a
* sendable byte slice.
*/
for _, line := range gc.sections {
content, gophorErr := line.Render(request)
func (gc *GophermapContents) Render(responder *Responder) *GophorError {
/* Render and send each of the gophermap sections */
var gophorErr *GophorError
for _, line := range gc.Sections {
gophorErr = line.Render(responder)
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 */
return returnContents
/* End on footer text (including lastline) */
return responder.WriteData(Config.FooterText)
}
func (gc *GophermapContents) Load() *GophorError {
/* Load the gophermap into memory as gophermap sections */
var gophorErr *GophorError
gc.sections, gophorErr = readGophermap(gc.path)
return gophorErr
gc.Sections, gophorErr = readGophermap(gc.Request)
if gophorErr != nil {
return &GophorError{ InvalidGophermapErr, gophorErr }
} else {
return nil
}
}
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 {
Render(*FileSystemRequest) ([]byte, *GophorError)
/* Interface for storing differring types of gophermap
* sections to render when necessary
*/
Render(*Responder) *GophorError
}
/* GophermapText:
* Simple implementation of GophermapSection that holds
* onto a static section of text as a slice of bytes.
*/
type GophermapText struct {
type GophermapTextSection struct {
Contents []byte
}
func NewGophermapText(contents []byte) *GophermapText {
return &GophermapText{ contents }
func (s *GophermapTextSection) Render(responder *Responder) *GophorError {
return responder.WriteData(replaceStrings(string(s.Contents), responder.Host))
}
func (s *GophermapText) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return replaceStrings(string(s.Contents), request.Host), nil
type GophermapDirectorySection struct {
/* Holds onto a directory path, and a list of files
* to hide from the client when rendering.
*/
Request *Request
Hidden map[string]bool
}
/* GophermapDirListing:
* An implementation of GophermapSection that holds onto a
* path and a requested list of hidden files, then enumerates
* the supplied paths (ignoring hidden files) when the content
* Render() call is received.
*/
type GophermapDirListing struct {
Path string
Hidden map[string]bool
func (g *GophermapDirectorySection) Render(responder *Responder) *GophorError {
/* Create new responder from supplied and using stored path */
return listDir(responder.CloneWithRequest(g.Request), g.Hidden)
}
func NewGophermapDirListing(path string) *GophermapDirListing {
return &GophermapDirListing{ path, nil }
type GophermapFileSection struct {
/* Holds onto a file path to be read and rendered when requested */
Request *Request
}
func (s *GophermapDirListing) Render(request *FileSystemRequest) ([]byte, *GophorError) {
/* We could just pass the request directly, but in case the request
* path happens to differ for whatever reason we create a new one
*/
return listDir(&FileSystemRequest{ s.Path, request.Host }, s.Hidden)
func (g *GophermapFileSection) Render(responder *Responder) *GophorError {
fileContents, gophorErr := readIntoGophermap(g.Request.Path.Absolute())
if gophorErr != nil {
return gophorErr
}
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 */
sections := make([]GophermapSection, 0)
/* _Create_ hidden files map now in case dir listing requested */
hidden := make(map[string]bool)
/* Create hidden files map now in case dir listing requested */
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!) */
titleAlready := false
/* Reference directory listing now in case requested */
var dirListing *GophermapDirListing
/* Error setting within nested function below */
var returnErr *GophorError
/* Perform buffered scan with our supplied splitter and iterators */
gophorErr := bufferedScan(path,
gophorErr := bufferedScan(request.Path.Absolute(),
func(scanner *bufio.Scanner) bool {
line := scanner.Text()
@ -170,12 +211,12 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
switch lineType {
case TypeInfoNotStated:
/* Append TypeInfo to the beginning of line */
sections = append(sections, NewGophermapText(buildInfoLine(line)))
sections = append(sections, &GophermapTextSection{ buildInfoLine(line) })
case TypeTitle:
/* Reformat title line to send as info line with appropriate selector */
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
}
@ -185,59 +226,65 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
case TypeHiddenFile:
/* Add to hidden files map */
hidden[line[1:]] = true
hidden[request.Path.JoinRel(line[1:])] = true
case TypeSubGophermap:
/* Check if we've been supplied subgophermap or regular file */
if strings.HasSuffix(line[1:], GophermapFileStr) {
/* Ensure we haven't been passed the current gophermap. Recursion bad! */
if line[1:] == path {
break
}
/* Parse new RequestPath and parameters */
subRequest, gophorErr := parseLineRequestString(request.Path, line[1:])
if gophorErr != nil {
/* Failed parsing line request string, set returnErr and request finish */
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! */
submapSections, gophorErr := readGophermap(line[1:])
if gophorErr != nil {
/* Failed to read subgophermap, insert error line */
sections = append(sections, NewGophermapText(buildInfoLine("Error reading subgophermap: "+line[1:])))
/* Check if we've been supplied subgophermap or regular file */
if isGophermap(subRequest.Path.Relative()) {
/* If executable, store as GophermapExecFileSection, else GophermapSubmapSection */
if stat.Mode().Perm() & 0100 != 0 {
sections = append(sections, &GophermapExecFileSection { subRequest })
} else {
sections = append(sections, submapSections...)
sections = append(sections, &GophermapSubmapSection{ subRequest })
}
} else {
/* Treat as regular file, but we need to replace Unix line endings
* with gophermap line endings
*/
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:])))
/* If stored in cgi-bin store as GophermapExecCgiSection, else GophermapFileSection */
if withinCgiBin(subRequest.Path.Relative()) {
sections = append(sections, &GophermapExecCgiSection{ subRequest })
} 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:
/* Lastline, break out at end of loop. Interface method Contents()
* will append a last line at the end so we don't have to worry about
* that here, only stopping the loop.
/* Lastline, break out at end of loop. GophermapContents.Render() will
* append a LastLine string so we don't have to worry about that here.
*/
return false
case TypeEndBeginList:
/* Create GophermapDirListing object then break out at end of loop */
dirListing = NewGophermapDirListing(strings.TrimSuffix(path, GophermapFileStr))
/* Append GophermapDirectorySection object then break, as with TypeEnd. */
dirRequest := &Request{ NewRequestPath(request.Path.RootDir(), request.Path.TrimRelSuffix(GophermapFileStr)), "" }
sections = append(sections, &GophermapDirectorySection{ dirRequest, hidden })
return false
default:
/* Just append to sections slice as gophermap text */
sections = append(sections, NewGophermapText([]byte(line+DOSLineEnd)))
/* Default is appending to sections slice as GopherMapTextSection */
sections = append(sections, &GophermapTextSection{ []byte(line+DOSLineEnd) })
}
return true
},
)
@ -245,26 +292,19 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
/* Check the bufferedScan didn't exit with error */
if gophorErr != nil {
return nil, gophorErr
}
/* 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)
} else if returnErr != nil {
return nil, returnErr
}
return sections, nil
}
/* Read a text file into a gophermap as text sections */
func readIntoGophermap(path string) ([]byte, *GophorError) {
/* Create return slice */
fileContents := make([]byte, 0)
/* Perform buffered scan with our supplied splitter and iterators */
/* Perform buffered scan with our supplied iterator */
gophorErr := bufferedScan(path,
func(scanner *bufio.Scanner) bool {
line := scanner.Text()
@ -274,10 +314,10 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
return true
}
/* Replace the newline character */
line = strings.Replace(line, "\n", "", -1)
/* Replace the newline characters */
line = replaceNewLines(line)
/* Iterate through returned str, reflowing to new line
/* Iterate through line string, reflowing to new line
* until all lines < PageWidth
*/
for len(line) > 0 {
@ -285,7 +325,7 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
fileContents = append(fileContents, buildInfoLine(line[:length])...)
line = line[length:]
}
return true
},
)
@ -303,6 +343,7 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
return fileContents, nil
}
/* Return minimum width out of PageWidth and W */
func minWidth(w int) int {
if w <= Config.PageWidth {
return w
@ -310,9 +351,3 @@ func minWidth(w int) int {
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 (
"os"
"sync"
"path"
"time"
"strings"
"regexp"
)
type FileType int
const (
/* Leads to some more concise code below */
FileTypeRegular FileType = iota
FileTypeDir FileType = iota
FileTypeBad FileType = iota
/* Help converting file size stat to supplied size in megabytes */
BytesInMegaByte = 1048576.0
/* 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 {
/* Holds and helps manage our file cache, as well as managing
* access and responses to requests submitted a worker instance.
*/
CacheMap *FixedMap
CacheMutex sync.RWMutex
CacheFileMax int64
Remaps []*FileRemap
Restricted []*regexp.Regexp
}
func (fs *FileSystem) Init(size int, fileSizeMax float64) {
fs.CacheMap = NewFixedMap(size)
fs.CacheMutex = sync.RWMutex{}
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
/* .Remaps and .Restricted are handled within gopher.go */
}
func (fs *FileSystem) HandleRequest(requestPath string, host *ConnHost) ([]byte, *GophorError) {
/* Stat filesystem for request's file type */
fileType := FileTypeDir;
if requestPath != "/" {
stat, err := os.Stat(requestPath)
if err != nil {
/* Check file isn't in cache before throwing in the towel */
fs.CacheMutex.RLock()
file := fs.CacheMap.Get(requestPath)
if file == nil {
fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
}
func (fs *FileSystem) IsRestricted(path string) bool {
for _, regex := range fs.Restricted {
if regex.MatchString(path) {
return true
}
}
return false
}
/* It's there! Get contents, unlock and return */
file.Mutex.RLock()
b := file.Contents(&FileSystemRequest{ requestPath, host })
file.Mutex.RUnlock()
func (fs *FileSystem) RemapRequestPath(requestPath *RequestPath) (*RequestPath, bool) {
for _, remap := range fs.Remaps {
/* No match :( keep lookin */
if !remap.Regex.MatchString(requestPath.Relative()) {
continue
}
fs.CacheMutex.RUnlock()
return b, nil
/* Create new path from template and submatches */
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 */
switch {
case stat.Mode() & os.ModeDir != 0:
/* do nothing, already set :) */
break
/* Set this new path to the _actual_ path */
return requestPath.RemapPath(string(newPath)), true
}
return nil, false
}
case stat.Mode() & os.ModeType == 0:
fileType = FileTypeRegular
func (fs *FileSystem) HandleRequest(responder *Responder) *GophorError {
/* Check if restricted file */
if fs.IsRestricted(responder.Request.Path.Relative()) {
return &GophorError{ IllegalPathErr, nil }
}
default:
fileType = FileTypeBad
/* Try remap according to supplied regex */
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 */
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 */
gophermapPath := path.Join(requestPath, GophermapFileStr)
_, err := os.Stat(gophermapPath)
gophermapPath := NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(GophermapFileStr))
stat, err = os.Stat(gophermapPath.Absolute())
var output []byte
var gophorErr *GophorError
if err == nil {
/* Gophermap exists, serve this! */
output, gophorErr = fs.FetchFile(&FileSystemRequest{ gophermapPath, host })
/* Gophermap exists! If executable try return executed contents, else serve as regular gophermap. */
gophermapRequest := &Request{ gophermapPath, responder.Request.Parameters }
responder.Request = gophermapRequest
if stat.Mode().Perm() & 0100 != 0 {
return executeFile(responder)
} else {
return fs.FetchFile(responder)
}
} else {
/* No gophermap, serve directory listing */
output, gophorErr = listDir(&FileSystemRequest{ requestPath, host }, map[string]bool{})
}
if gophorErr != nil {
/* Fail out! */
return nil, gophorErr
return listDirAsGophermap(responder, map[string]bool{ gophermapPath.Relative(): true, CgiBinDirStr: true })
}
/* Append footer text (contains last line) and return */
output = append(output, Config.FooterText...)
return output, nil
/* Regular file */
case FileTypeRegular:
return fs.FetchFile(&FileSystemRequest{ requestPath, host })
case stat.Mode() & os.ModeType == 0:
/* 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 */
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 */
fs.CacheMutex.RLock()
file := fs.CacheMap.Get(request.Path)
file := fs.CacheMap.Get(responder.Request.Path.Absolute())
if file != nil {
/* 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()
/* Reload file contents from disk */
gophorErr := file.LoadContents()
gophorErr := file.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
file.Mutex.Unlock()
fs.CacheMutex.RUnlock()
return nil, gophorErr
return gophorErr
}
/* Updated! Swap back file write for read lock */
@ -136,42 +188,48 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
file.Mutex.RLock()
}
} else {
/* Perform filesystem stat ready for checking file size later.
* Doing this now allows us to weed-out non-existent files early
/* Open file here, to check it exists, ready for file stat
* 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 {
/* Error stat'ing file, unlock read mutex then return error */
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
if strings.HasSuffix(request.Path, "/"+GophermapFileStr) {
contents = &GophermapContents{ request.Path, nil }
if isGophermap(responder.Request.Path.Relative()) {
contents = &GophermapContents{ responder.Request, nil }
} else {
contents = &RegularFileContents{ request.Path, nil }
contents = &RegularFileContents{ responder.Request.Path, nil }
}
/* 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 */
gophorErr := file.LoadContents()
gophorErr := file.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock read mutex then return error */
fs.CacheMutex.RUnlock()
return nil, 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
return gophorErr
}
/* 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()
/* 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() */
file.Mutex.RLock()
@ -189,84 +247,48 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
fs.CacheMutex.RLock()
}
/* Read file contents into new variable for return, then unlock file read lock */
b := file.Contents(request)
/* Write file contents via responder */
gophorErr := file.WriteContents(responder)
file.Mutex.RUnlock()
/* Finally we can unlock the cache map read lock, we are done :) */
fs.CacheMutex.RUnlock()
return b, nil
}
/* 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
return gophorErr
}
/* File:
* Wraps around the cached contents of a file and
* helps with management of this content by the
* global FileCache objects.
*/
type File struct {
contents FileContents
/* Wraps around the cached contents of a file
* helping with management.
*/
Content FileContents
Mutex sync.RWMutex
Fresh bool
LastRefresh int64
}
func NewFile(contents FileContents) *File {
return &File{
contents,
sync.RWMutex{},
true,
0,
}
}
func (f *File) Contents(request *FileSystemRequest) []byte {
return f.contents.Render(request)
func (f *File) WriteContents(responder *Responder) *GophorError {
return f.Content.Render(responder)
}
func (f *File) LoadContents() *GophorError {
func (f *File) CacheContents() *GophorError {
/* Clear current file contents */
f.contents.Clear()
f.Content.Clear()
/* Reload the file */
gophorErr := f.contents.Load()
gophorErr := f.Content.Load()
if gophorErr != nil {
return gophorErr
}
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.LastRefresh = time.Now().UnixNano()
f.Fresh = true
f.Fresh = true
return nil
}
/* FileContents:
* 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()
}
/* Start the file monitor! */
func startFileMonitor(sleepTime time.Duration) {
go func() {
for {
@ -278,10 +300,11 @@ func startFileMonitor(sleepTime time.Duration) {
}
/* 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() {
/* Before anything, get cache write lock (in case we have to delete) */
Config.FileSystem.CacheMutex.Lock()
@ -296,16 +319,18 @@ func checkCacheFreshness() {
continue
}
/* Check file still exists on disk, delete and continue if not */
stat, err := os.Stat(path)
if err != nil {
/* Log file as not in cache, then delete */
Config.LogSystemError("Failed to stat file in cache: %s\n", path)
Config.SysLog.Error("", "Failed to stat file in cache: %s\n", path)
Config.FileSystem.CacheMap.Remove(path)
continue
}
/* Get file's last modified time */
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 {
file.Fresh = false
}
@ -315,9 +340,9 @@ func checkCacheFreshness() {
Config.FileSystem.CacheMutex.Unlock()
}
/* Just a helper function to neaten-up checking if file contents is of generated type */
func isGeneratedType(file *File) bool {
/* Just a helper function to neaten-up checking if file contents is of generated type */
switch file.contents.(type) {
switch file.Content.(type) {
case *GeneratedFileContents:
return true
default:

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

@ -38,14 +38,15 @@ func NewFixedMap(size int) *FixedMap {
func (fm *FixedMap) Get(key string) *File {
elem, ok := fm.Map[key]
if ok {
/* And that's an LRU implementation folks! */
fm.List.MoveToFront(elem.Element)
return elem.Value
} else {
return nil
}
}
/* Put file in map as key, pushing out last file
* if size limit reached */
/* Put file in map as key, pushing out last file if size limit reached */
func (fm *FixedMap) Put(key string, value *File) {
element := fm.List.PushFront(key)
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! */
delete(fm.Map, key)
fm.List.Remove(element)
Config.LogSystem("Popped key: %s\n", key)
}
}

@ -4,152 +4,6 @@ import (
"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) */
func formatGophermapFooter(text string, useSeparator bool) []byte {
ret := make([]byte, 0)
@ -162,51 +16,35 @@ func formatGophermapFooter(text string, useSeparator bool) []byte {
ret = append(ret, buildInfoLine(line)...)
}
}
ret = append(ret, []byte(LastLine)...)
return ret
return append(ret, []byte(LastLine)...)
}
/* Parse line type from contents */
func parseLineType(line string) ItemType {
lineLen := len(line)
/* Replace standard replacement strings */
func replaceStrings(str string, connHost *ConnHost) []byte {
/* We only replace the actual host and port values */
split := strings.Split(str, Tab)
if len(split) < 4 {
return []byte(str)
}
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
case TypeExec:
return TypeExec
default:
return TypeInfoNotStated
}
split[2] = strings.Replace(split[2], ReplaceStrHostname, connHost.Name(), -1)
split[3] = strings.Replace(split[3], ReplaceStrPort, connHost.Port(), -1)
/* Return slice */
b := make([]byte, 0)
/* Recombine the slices and add the removed tabs */
splitLen := len(split)
for i := 0; i < splitLen-1; i += 1 {
split[i] += Tab
b = append(b, []byte(split[i])...)
}
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 (
"os"
"os/user"
"log"
"strconv"
"syscall"
"os/signal"
@ -10,23 +10,9 @@ import (
"time"
)
/*
* GoLang's built-in syscall.{Setuid,Setgid}() methods don't work as expected (all I ever
* 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"
const (
GophorVersion = "1.0-beta"
)
var (
Config *ServerConfig
@ -38,31 +24,27 @@ func main() {
/* Handle signals so we can _actually_ shutdowm */
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 */
for _, l := range listeners {
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 {
newConn, err := l.Accept()
worker, err := l.Accept()
if err != nil {
Config.LogSystemError("Error accepting connection: %s\n", err.Error())
Config.SysLog.Error("", "Error accepting connection: %s\n", err.Error())
continue
}
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
NewWorker(newConn).Serve()
}()
go worker.Serve()
}
}()
}
/* When OS signal received, we close-up */
sig := <-signals
Config.LogSystem("Signal received: %v. Shutting down...\n", sig)
Config.SysLog.Info("", "Signal received: %v. Shutting down...\n", sig)
os.Exit(0)
}
@ -70,37 +52,63 @@ func setupServer() []*GophorListener {
/* First we setup all the flags and parse them... */
/* Base server settings */
serverRoot := flag.String("root", "/var/gopher", "Change server root directory.")
serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
serverPort := flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).")
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.")
serverRoot := flag.String("root", "/var/gopher", "Change server root directory.")
serverBindAddr := flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
serverPort := flag.Int("port", 70, "Change server bind port.")
/* User supplied caps.txt information */
serverDescription := flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in generated caps.txt.")
serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.")
serverFwdPort := flag.Int("fwd-port", 0, "Change port used in '$port' replacement strings (useful if you're port forwarding).")
serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
/* 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 */
footerText := flag.String("footer", "", "Change gophermap footer text (Unix new-line separated lines).")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
// charSet := flag.String("charset", "", "Change default output charset.")
charSet := "utf-8"
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting files from showing in directory listings.")
footerText := flag.String("footer", " Gophor, a Gopher server in Go.", "Change gophermap footer text (Unix new-line separated lines).")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
/* 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).")
logType := flag.Int("log-type", 0, "Change server log file handling -- 0:default 1:disable")
/* Regex */
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting accessible files.")
fileRemaps := flag.String("file-remap", "", "New-line separated list of file remappings of format: /virtual/relative/path -> /actual/relative/path")
/* Cache settings */
cacheCheckFreq := flag.String("cache-check", "60s", "Change file cache freshness check frequency.")
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.")
/* User supplied caps.txt information */
serverDescription := flag.String("description", "Gophor, a Gopher server in Go.", "Change server description in generated caps.txt.")
serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.")
/* 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 := flag.Bool("version", false, "Print version information.")
version := flag.Bool("version", false, "Print version information.")
/* Parse parse parse!! */
flag.Parse()
@ -110,104 +118,117 @@ func setupServer() []*GophorListener {
/* Setup the server configuration instance and enter as much as we can right now */
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 */
Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator)
Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator)
/* Setup Gophor logging system */
Config.SystemLogger, Config.AccessLogger = setupLogging(*logType, *systemLogPath, *accessLogPath)
/* Get UID + GID for requested user. Has to be done BEFORE chroot or it fails */
var uid, gid int
if *execAs == "" {
/* No 'execAs' user specified, try run as default user account permissions */
uid = 1000
gid = 1000
} else if *execAs == "root" {
/* Naughty, naughty! */
Config.LogSystemFatal("Gophor does not support directly running as root\n")
Config.SysLog, Config.AccLog = setupLoggers(*logOutput, *logOpts, *systemLogPath, *accessLogPath)
/* Set CGI support status */
if *disableCgi {
Config.SysLog.Info("", "CGI support disabled\n")
Config.CgiEnabled = false
} else {
/* Try lookup specified username */
user, err := user.Lookup(*execAs)
if err != nil {
Config.LogSystemFatal("Error getting information for requested user %s: %s\n", *execAs, err)
/* Enable CGI */
Config.SysLog.Info("", "CGI support enabled\n")
Config.CgiEnabled = true
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 */
uid, _ = strconv.Atoi(user.Uid)
gid, _ = strconv.Atoi(user.Gid)
/* Set safe executable path and setup environments */
Config.SysLog.Info("", "Setting safe executable path: %s\n", *safeExecPath)
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 */
enterServerDir(*serverRoot)
Config.LogSystem("Entered server directory: %s\n", *serverRoot)
/* Try enter chroot if requested */
chrootServerDir(*serverRoot)
Config.LogSystem("Chroot success, new root: %s\n", *serverRoot)
Config.SysLog.Info("", "Entered server directory: %s\n", *serverRoot)
/* Setup listeners */
listeners := make([]*GophorListener, 0)
/* If requested, setup unencrypted listener */
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 {
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)
} else {
Config.LogSystemFatal("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
log.Fatalf("No valid port to listen on\n")
}
/* Setup file cache */
Config.FileSystem = new(FileSystem)
/* Check if cache requested disabled */
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 */
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,
* check if we need to cache generated policy files
*/
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc)
cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
/* Start file cache freshness checker */
go startFileMonitor(fileMonitorSleepTime)
Config.LogSystem("File cache freshness monitor started with frequency: %s\n", fileMonitorSleepTime)
startFileMonitor(*fileMonitorFreq)
Config.SysLog.Info("", "File caching enabled with: maxcount=%d maxsize=%.3fMB checkfreq=%s\n", *cacheSize, *cacheFileSizeMax, *fileMonitorFreq)
} else {
/* File caching disabled, init with zero max size so nothing gets cached */
Config.FileSystem.Init(2, 0)
Config.LogSystem("File caching disabled\n")
Config.SysLog.Info("", "File caching disabled\n")
/* 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 listeners
}
@ -215,47 +236,6 @@ func setupServer() []*GophorListener {
func enterServerDir(path string) {
err := syscall.Chdir(path)
if err != nil {
Config.LogSystemFatal("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)
}
log.Fatalf("Error changing dir to server root %s: %s\n", path, err.Error())
}
}

@ -1,5 +1,6 @@
package main
/* Function does, as function is named */
func generateHtmlRedirect(url string) []byte {
content :=
"<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 (
"log"
"os"
"io"
"io/ioutil"
"strings"
)
func setupLogging(loggingType int, systemLogPath, accessLogPath string) (*log.Logger, *log.Logger) {
/* Setup global logger */
log.SetOutput(os.Stderr)
log.SetFlags(0)
const (
/* Prefixes */
LogPrefixInfo = ": I :: "
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* */
useSame := (systemLogPath == accessLogPath)
/* Check requested logging type */
var systemLogger, accessLogger *log.Logger
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
case LogToStderr:
/* Return two separate stderr loggers */
sysLogger := &LoggerNoPrefix{ NewLoggerToStderr(logFlags) }
if logIps {
return sysLogger, &Logger{ NewLoggerToStderr(logFlags) }
} 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 */
if useSame {
accessLogger = systemLogger
}
case LogDisabled:
/* Return two pointers to same null logger */
nullLogger := &NullLogger{}
return nullLogger, nullLogger
/* Setup access logger to output to file, or stderr if none supplied */
var accessWriter io.Writer
if accessLogPath != "" {
fd, err := os.OpenFile(accessLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalf("Failed to create access logger: %s\n", err.Error())
}
accessWriter = fd
case LogToFile:
/* Return two separate file loggers */
sysLogger := &Logger{ NewLoggerToFile(systemLogPath, logFlags) }
if logIps {
return sysLogger, &Logger{ NewLoggerToFile(accessLogPath, logFlags) }
} 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:
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() {
/* Reset the flags before printing version */
log.SetFlags(0)
log.Printf("%s\n", GophorVersion)
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 (
"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 */
_, err := os.Stat("/caps.txt")
_, err := os.Stat(path.Join(rootDir, CapsTxtStr))
if err != nil {
/* We need to generate the caps txt and manually load into cache */
content := generateCapsTxt(description, admin, geoloc)
/* Create new file object from generated file contents */
fileContents := &GeneratedFileContents{ content }
file := NewFile(fileContents)
file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* 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 */
Config.FileSystem.CacheMap.Put("/caps.txt", file)
Config.LogSystem("Generated policy file: /caps.txt\n")
Config.FileSystem.CacheMap.Put(rootDir+"/"+CapsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+CapsTxtStr)
}
/* See if caps txt exists, if not generate */
_, err = os.Stat("/robots.txt")
/* See if robots txt exists, if not generate */
_, err = os.Stat(rootDir+"/"+RobotsTxtStr)
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()
/* Create new file object from generated file contents */
fileContents := &GeneratedFileContents{ content }
file := NewFile(fileContents)
file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* 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 */
Config.FileSystem.CacheMap.Put("/robots.txt", file)
Config.LogSystem("Generated policy file: /robots.txt\n")
Config.FileSystem.CacheMap.Put(rootDir+"/"+RobotsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+RobotsTxtStr)
}
}
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 {
text := "CAPS"+DOSLineEnd
text += DOSLineEnd
text += "# This is an automatically generated"+DOSLineEnd
text += "# server policy file: caps.txt"+DOSLineEnd
text += generatePolicyHeader(CapsTxtStr)
text += DOSLineEnd
text += "CapsVersion=1"+DOSLineEnd
text += "ExpireCapsAfter=1800"+DOSLineEnd
@ -64,14 +77,16 @@ func generateCapsTxt(description, admin, geoloc string) []byte {
text += "ServerSoftwareVersion="+GophorVersion+DOSLineEnd
text += "ServerDescription="+description+DOSLineEnd
text += "ServerGeolocationString="+geoloc+DOSLineEnd
text += "ServerDefaultEncoding=ascii"+DOSLineEnd
// text += "ServerDefaultEncoding=ascii"+DOSLineEnd
text += DOSLineEnd
text += "ServerAdmin="+admin+DOSLineEnd
return []byte(text)
}
func generateRobotsTxt() []byte {
text := "Usage-agent: *"+DOSLineEnd
text := generatePolicyHeader(RobotsTxtStr)
text += DOSLineEnd
text += "Usage-agent: *"+DOSLineEnd
text += "Disallow: *"+DOSLineEnd
text += DOSLineEnd
text += "Crawl-delay: 99999"+DOSLineEnd

@ -3,32 +3,92 @@ package main
import (
"regexp"
"strings"
"log"
)
func compileUserRestrictedFilesRegex(restrictedFiles string) []*regexp.Regexp {
Config.LogSystem("Compiling restricted file regular expressions\n")
const (
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 */
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 */
for _, expr := range strings.Split(restrictedFiles, "\n") {
/* Try compile regex */
regex, err := regexp.Compile(expr)
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 */
func isRestrictedFile(name string) bool {
for _, regex := range Config.RestrictedFiles {
if regex.MatchString(name) {
return true
/* Compile a user supplied new line separated list of file remap regex statements */
func compileUserRemapRegex(remaps string) []*FileRemap {
/* Return slice */
fileRemaps := make([]*FileRemap, 0)
/* 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
import (
"path"
"strings"
)
type Worker struct {
Conn *GophorConn
}
func NewWorker(conn *GophorConn) *Worker {
return &Worker{ conn }
Conn *BufferedDeadlineConn
Host *ConnHost
Client *ConnClient
RootDir string
}
func (worker *Worker) Serve() {
defer func() {
/* 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())
defer worker.Conn.Close()
/* Generate response bytes from error code */
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)
line, err := worker.Conn.ReadLine()
if err != nil {
return &GophorError{ SocketWriteErr, err }
} else if count != len(b) {
return &GophorError{ SocketWriteCountErr, nil }
Config.SysLog.Error("", "Error reading from socket port %s: %s\n", worker.Host.Port(), err.Error())
return
}
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{}) {
Config.LogAccessError(worker.Conn.RemoteAddr().String(), format, args...)
}
func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* According to Gopher spec, only read up to first Tab or Crlf */
dataStr := readUpToFirstTabOrCrlf(data)
/* Drop up to first tab */
received := strings.Split(string(line), Tab)[0]
/* Handle URL request if presented */
lenBefore := len(dataStr)
dataStr = strings.TrimPrefix(dataStr, "URL:")
switch len(dataStr) {
lenBefore := len(received)
received = strings.TrimPrefix(received, "URL:")
switch len(received) {
case lenBefore-4:
/* Send an HTML redirect to supplied URL */
worker.Log("Redirecting to %s\n", dataStr)
return worker.SendRaw(generateHtmlRedirect(dataStr))
Config.AccLog.Info("("+worker.Client.Ip()+") ", "Redirecting to %s\n", received)
worker.Conn.Write(generateHtmlRedirect(received))
return
default:
/* Do nothing */
}
/* Sanitize supplied path */
requestPath := sanitizePath(dataStr)
/* Append lastline */
response, gophorErr := Config.FileSystem.HandleRequest(requestPath, worker.Conn.Host)
if gophorErr != nil {
worker.LogError("Failed to serve: %s\n", requestPath)
return gophorErr
}
worker.Log("Served: %s\n", requestPath)
/* Create GopherUrl object from request string */
url, gophorErr := parseGopherUrl(received)
if gophorErr == nil {
/* Create new request from url object */
request := NewSanitizedRequest(worker.RootDir, url)
/* Serve response */
return worker.SendRaw(response)
}
/* Create new responder from request */
responder := NewResponder(worker.Conn, worker.Host, worker.Client, request)
func readUpToFirstTabOrCrlf(data []byte) string {
/* Only read up to first tab or cr-lf */
dataStr := ""
dataLen := len(data)
for i := 0; i < dataLen; i += 1 {
switch data[i] {
case '\t':
return dataStr
case DOSLineEnd[0]:
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
return dataStr
}
default:
dataStr += string(data[i])
/* Handle request with supplied responder */
gophorErr = Config.FileSystem.HandleRequest(responder)
if gophorErr == nil {
/* Log success to access and return! */
responder.AccessLogInfo("Served: %s\n", request.Path.Absolute())
return
} else {
/* Log failure to access */
responder.AccessLogError("Failed to serve: %s\n", request.Path.Absolute())
}
}
return dataStr
}
/* Log serve failure to error to system */
Config.SysLog.Error("", gophorErr.Error())
func sanitizePath(dataStr string) string {
/* Clean path and trim '/' prefix if still exists */
requestPath := strings.TrimPrefix(path.Clean(dataStr), "/")
/* Generate response bytes from error code */
errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
if requestPath == "." {
requestPath = "/"
} else if !strings.HasPrefix(requestPath, "/") {
requestPath = "/" + requestPath
/* If we got response bytes to send? SEND 'EM! */
if errResponse != nil {
/* No gods. No masters. We don't care about error checking here */
worker.Conn.WriteData(errResponse)
}
return requestPath
}

Loading…
Cancel
Save