Merge pull request #9 from grufwub/development

v0.5-alpha development pull
master
Kim 4 years ago committed by GitHub
commit b7a78597e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

2
.gitignore vendored

@ -1,4 +1,4 @@
gophor
*.log
*.old
build/
build*/

@ -1,40 +1,63 @@
# Gophor
Holy shit we made it to our first alpha release!
A Gopher server written in GoLang as a means of learning about the Gopher
protocol, and more GoLang.
Linux only _for now_. Cross-compiled to way too many architectures. Don't
judge my build script, it's not easy on the eyes, I'll fix it when I can be
bothered...
Linux only _for now_. Cross-compiled to way too many architectures.
Build-script now much improved, but still not pretty...
I'm unemployed and work on open-source projects like this and many others for
free. If you would like to help support my work that would be hugely
appreciated 💕 https://liberapay.com/grufwub/
`gophor-run` is an example script to help with automation of gophor.
WARNING: the development branch is filled with lava, fear and capitalism.
# Usage
```
gophor [args]
-root Change server root directory.
-port Change server NON-TLS listening port.
-hostname Change server hostname (FQDN, used to craft dir lists).
-bind-addr Change server bind-address (used in creating socket).
-user Drop to supplied user's UID and GID permissions before execution.
-system-log Path to gophor system log file, else use stderr.
-access-log Path to gophor access log file, else use stderr.
-cache-check Change file-cache freshness check frequency.
-cache-size Change max no. files in file-cache.
-cache-file-max Change maximum allowed size of a cached file.
-page-width Change page width used when formatting output.
-restrict-files New-line separated list of regex statements restricting
files from showing in directory listing.
-description Change server description in auto generated caps.txt.
-admin-email Change admin email in auto generated caps.txt.
-geoloc Change geolocation in auto generated caps.txt.
-root Change server root directory.
-port Change server NON-TLS listening port.
-hostname Change server hostname (FQDN, used to craft dir
lists).
-bind-addr Change server bind-address (used in creating
socket).
-user Drop to supplied user's UID and GID permissions
before execution.
-system-log Path to gophor system log file, else use stderr.
-access-log Path to gophor access log file, else use stderr.
-cache-check Change file-cache freshness check frequency.
-cache-size Change max no. files in file-cache.
-cache-file-max Change maximum allowed size of a cached file.
-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
restricting files from showing in directory listing.
-description Change server description in generated caps.txt.
-admin-email Change admin email in generated caps.txt.
-geoloc Change geolocation in generated caps.txt.
-version Print version string.
```
# Features
@ -176,38 +199,37 @@ files.
## Placeholder text
Selector: `-`
All of the following are used as placeholder text in responses...
Host: `null.host`
Null selector: `-`
Port: `0`
Null host: `null.host`
Null port: `0`
# Todos
Shortterm:
- Add last-mod-time to directory listings -- have global time parser object
- Set default charset -- need to think about implementation here...
- 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?
- 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.
- Set default charset -- need to think about implementation here...
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.
- 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.
- Improve responding to policy files + URL redirects -- need to rethink the
worker + response logic.
- Add more files to file extension map
- 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?
Longterm:
- 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?)
- TLS support -- ~~requires a rethink of how we're passing port functions
generating gopher directory entries, also there is no definitive standard
@ -218,9 +240,6 @@ Longterm:
recently connected IPs. Keep incremementing connection count and only
remove from list when `lastIncremented` time is greater than timeout
- Header + footer text -- read in file / input string and format, hold in
memory than append to end of gophermaps / dir listings
- 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

@ -3,13 +3,40 @@
set -e
PROJECT='gophor'
OUTDIR='build'
VERSION="$(cat 'constants.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')"
echo "VERSION: $VERSION"
GOVERSION="$(go version | sed -e 's|^go version go||' -e 's|\s.*$||')"
echo "PLEASE BE WARNED THIS SCRIPT IS WRITTEN FOR MY VOID LINUX BUILD ENVIRONMENT"
echo "YOUR CC CROSS-COMPILER LOCATIONS MAY DIFFER ON YOUR BUILD SYSTEM"
LOGFILE='build.log'
OUTDIR="build-${VERSION}"
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 ""
}
echo "PLEASE BE WARNED THIS SCRIPT IS WRITTEN FOR A VOID LINUX (MUSL) BUILD ENVIRONMENT"
echo "YOUR CC TOOLCHAIN LOCATIONS MAY DIFFER"
echo "IF THE SCRIPT FAILS, CHECK THE OUTPUT OF: ${LOGFILE}"
echo ""
# Clean and recreate directory
@ -17,95 +44,28 @@ rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
# Build time :)
echo "Building for linux 386..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.386_${GOVERSION}"
CGO_ENABLED=1 CC='i686-linux-musl-gcc' GOOS='linux' GOARCH='386' go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for '386' 'i686-linux-musl-gcc' 'linux' '386' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux amd64..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.amd64_${GOVERSION}"
CGO_ENABLED=1 CC='x86_64-linux-musl-gcc' GOOS='linux' GOARCH='amd64' go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'amd64' 'x86_64-linux-musl-gcc' 'linux' 'amd64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux armv5..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.armv5_${GOVERSION}"
CGO_ENABLED=1 CC='arm-linux-musleabi-gcc' GOOS='linux' GOARCH='arm' GOARM=5 go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'armv5' 'arm-linux-musleabi-gcc' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux armv5hf..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.armv5hf_${GOVERSION}"
CGO_ENABLED=1 CC='arm-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=5 go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'armv5hf' 'arm-linux-musleabihf-gcc' 'linux' 'arm' '5' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux armv6..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.armv6_${GOVERSION}"
CGO_ENABLED=1 CC='arm-linux-musleabi-gcc' GOOS='linux' GOARCH='arm' GOARM=6 go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'armv6' 'arm-linux-musleabi-gcc' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux armv6hf..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.armv6hf_${GOVERSION}"
CGO_ENABLED=1 CC='arm-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=6 go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'armv6hf' 'arm-linux-musleabihf-gcc' 'linux' 'arm' '6' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux armv7hf..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.armv7hf_${GOVERSION}"
CGO_ENABLED=1 CC='armv7l-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=7 go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'armv7lhf' 'armv7l-linux-musleabihf-gcc' 'linux' 'arm' '7' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux arm64..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.arm64_${GOVERSION}"
CGO_ENABLED=1 CC='aarch64-linux-musl-gcc' GOOS='linux' GOARCH='arm64' go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'arm64' 'aarch64-linux-musl-gcc' 'linux' 'arm64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux mips..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.mips_${GOVERSION}"
CGO_ENABLED=1 CC='mips-linux-musl-gcc' GOOS='linux' GOARCH='mips' go build -trimpath -o "$filename" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'mips' 'mips-linux-musl-gcc' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux mipshf..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.mipshf_${GOVERSION}"
CGO_ENABLED=1 CC='mips-linux-muslhf-gcc' GOOS='linux' GOARCH='mips' go build -trimpath -o "$filename" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'mipshf' 'mips-linux-muslhf-gcc' 'linux' 'mips' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux mipsle..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.mipsle_${GOVERSION}"
CGO_ENABLED=1 CC='mipsel-linux-musl-gcc' GOOS='linux' GOARCH='mipsle' go build -trimpath -o "$filename" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
build_for 'mipsle' 'mipsel-linux-musl-gcc' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "Building for linux mipslehf..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.mipslehf_${GOVERSION}"
CGO_ENABLED=1 CC='mipsel-linux-muslhf-gcc' GOOS='linux' GOARCH='mipsle' go build -trimpath -o "$filename" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename"
echo ""
echo "Building for linux ppc64le..."
filename="${OUTDIR}/${PROJECT}.${VERSION}_linux.ppc64le_${GOVERSION}"
CGO_ENABLED=1 CC='powerpc64le-linux-musl-gcc' GOOS='linux' GOARCH='ppc64le' go build -trimpath -o "$filename" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best "$filename"
upx -t "$filename
echo ""
build_for 'mipslehf' 'mipsel-linux-muslhf-gcc' 'linux' 'mipsle' -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
echo "PLEASE DON'T JUDGE THIS SCRIPT, IT IS TRULY SO AWFUL. TO BE IMPROVED..."
build_for 'ppc64le' 'powerpc64le-linux-musl-gcc' 'linux' 'ppc64le' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'

@ -1,6 +1,38 @@
#!/bin/sh
echo "Building for current platform..."
CGO_ENABLED=1 go build -trimpath -o 'gophor' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx --best --color 'gophor'
echo ""
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,177 +0,0 @@
package main
import (
"os"
"sync"
"time"
)
func startFileMonitor(sleepTime time.Duration) {
go func() {
for {
/* Sleep so we don't take up all the precious CPU time :) */
time.Sleep(sleepTime)
/* Check global file cache freshness */
checkCacheFreshness()
}
/* We shouldn't have reached here */
Config.LogSystemFatal("FileCache monitor escaped run loop!\n")
}()
}
func checkCacheFreshness() {
/* Before anything, get cache write lock (in case we have to delete) */
Config.FileCache.CacheMutex.Lock()
/* Iterate through paths in cache map to query file last modified times */
for path := range Config.FileCache.CacheMap.Map {
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.FileCache.CacheMap.Remove(path)
continue
}
timeModified := stat.ModTime().UnixNano()
/* Get file pointer, no need for lock as we have write lock */
file := Config.FileCache.CacheMap.Get(path)
/* If the file is marked as fresh, but file on disk newer, mark as unfresh */
if file.IsFresh() && file.LastRefresh() < timeModified {
file.SetUnfresh()
}
}
/* Done! We can release cache read lock */
Config.FileCache.CacheMutex.Unlock()
}
/* FileCache:
* 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 FileCache struct {
CacheMap *FixedMap
CacheMutex sync.RWMutex
FileSizeMax int64
}
func (fc *FileCache) Init(size int, fileSizeMax float64) {
fc.CacheMap = NewFixedMap(size)
fc.CacheMutex = sync.RWMutex{}
fc.FileSizeMax = int64(BytesInMegaByte * fileSizeMax)
}
func (fc *FileCache) FetchRegular(request *FileSystemRequest) ([]byte, *GophorError) {
/* Calls fc.Fetch() but with the filecontents init function for a regular file */
return fc.Fetch(request, func(path string) FileContents {
contents := new(RegularFileContents)
contents.path = path
return contents
})
}
func (fc *FileCache) FetchGophermap(request *FileSystemRequest) ([]byte, *GophorError) {
/* Calls fc.Fetch() but with the filecontents init function for a gophermap */
return fc.Fetch(request, func(path string) FileContents {
contents := new(GophermapContents)
contents.path = path
return contents
})
}
func (fc *FileCache) Fetch(request *FileSystemRequest, newFileContents func(string) FileContents) ([]byte, *GophorError) {
/* Get cache map read lock then check if file in cache map */
fc.CacheMutex.RLock()
file := fc.CacheMap.Get(request.Path)
/* TODO: work on efficiency. improve use of mutex?? */
if file != nil {
/* File in cache -- before doing anything get file read lock */
file.RLock()
/* Check file is marked as fresh */
if !file.IsFresh() {
/* File not fresh! Swap file read for write-lock */
file.RUnlock()
file.Lock()
/* Reload file contents from disk */
gophorErr := file.LoadContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
file.Unlock()
fc.CacheMutex.RUnlock()
return nil, gophorErr
}
/* Updated! Swap back file write for read lock */
file.Unlock()
file.RLock()
}
} else {
/* Perform filesystem stat ready for checking file size later.
* Doing this now allows us to weed-out non-existent files early
*/
stat, err := os.Stat(request.Path)
if err != nil {
/* Error stat'ing file, unlock read mutex then return error */
fc.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
}
/* Create new file contents object using supplied function */
contents := newFileContents(request.Path)
/* Create new file wrapper around contents */
file = NewFile(contents)
/* NOTE: file isn't in cache yet so no need to lock file write mutex
* before loading contents from disk
*/
gophorErr := file.LoadContents()
if gophorErr != nil {
/* Error loading contents, unlock read mutex then return error */
fc.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() > fc.FileSizeMax {
b := file.Contents(request)
fc.CacheMutex.RUnlock()
return b, nil
}
/* File not in cache -- Swap cache map read for write lock. */
fc.CacheMutex.RUnlock()
fc.CacheMutex.Lock()
/* Put file in the FixedMap */
fc.CacheMap.Put(request.Path, file)
/* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */
file.RLock()
/* Swap cache lock back to read */
fc.CacheMutex.Unlock()
fc.CacheMutex.RLock()
}
/* Read file contents into new variable for return, then unlock file read lock */
b := file.Contents(request)
file.RUnlock()
/* Finally we can unlock the cache map read lock, we are done :) */
fc.CacheMutex.RUnlock()
return b, nil
}

@ -15,12 +15,8 @@ type ServerConfig struct {
/* Base settings */
RootDir string
/* Caps.txt information */
Description string
AdminEmail string
Geolocation string
/* Content settings */
FooterText []byte
PageWidth int
RestrictedFiles []*regexp.Regexp
@ -28,8 +24,8 @@ type ServerConfig struct {
SystemLogger *log.Logger
AccessLogger *log.Logger
/* Cache */
FileCache *FileCache
/* Filesystem access */
FileSystem *FileSystem
}
func (config *ServerConfig) LogSystem(fmt string, args ...interface{}) {
@ -45,9 +41,9 @@ func (config *ServerConfig) LogSystemFatal(fmt string, args ...interface{}) {
}
func (config *ServerConfig) LogAccess(sourceAddr, fmt string, args ...interface{}) {
config.AccessLogger.Printf(":: I :: ("+sourceAddr+") "+fmt, args...)
config.AccessLogger.Printf(":: I :: ["+sourceAddr+"] "+fmt, args...)
}
func (config *ServerConfig) LogAccessError(sourceAddr, fmt string, args ...interface{}) {
config.AccessLogger.Printf(":: E :: ("+sourceAddr+") "+fmt, args...)
config.AccessLogger.Printf(":: E :: ["+sourceAddr+"] "+fmt, args...)
}

@ -2,7 +2,12 @@ package main
const (
/* Gophor */
GophorVersion = "0.3-alpha"
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"
@ -12,6 +17,7 @@ const (
Tab = "\t"
LastLine = End+DOSLineEnd
/* Line creation */
MaxUserNameLen = 70 /* RFC 1436 standard */
MaxSelectorLen = 255 /* RFC 1436 standard */
@ -22,6 +28,7 @@ const (
SelectorErrorStr = "selector_length_error"
GophermapRenderErrorStr = ""
/* Replacement strings */
ReplaceStrHostname = "$hostname"
ReplaceStrPort = "$port"

@ -6,6 +6,27 @@ import (
"strings"
)
/* GeneratedFileContents:
* The simplest implementation of FileContents that
* stores some bytes and does nothing else.
*/
type GeneratedFileContents struct {
contents []byte
}
func (fc *GeneratedFileContents) Render(request *FileSystemRequest) []byte {
return fc.contents
}
func (fc *GeneratedFileContents) Load() *GophorError {
/* do nothing */
return nil
}
func (fc *GeneratedFileContents) Clear() {
/* do nothing */
}
/* RegularFileContents:
* Very simple implementation of FileContents that just
* buffered reads from the stored file path, stores the
@ -46,12 +67,13 @@ type GophermapContents struct {
}
func (gc *GophermapContents) Render(request *FileSystemRequest) []byte {
returnContents := make([]byte, 0)
/* 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.
*/
returnContents := make([]byte, 0)
for _, line := range gc.sections {
content, gophorErr := line.Render(request)
if gophorErr != nil {
@ -60,8 +82,7 @@ func (gc *GophermapContents) Render(request *FileSystemRequest) []byte {
returnContents = append(returnContents, content...)
}
/* Finally we end render with last line */
returnContents = append(returnContents, []byte(LastLine)...)
/* The footer added later contains last line, don't need to worry */
return returnContents
}
@ -93,17 +114,15 @@ type GophermapSection interface {
* onto a static section of text as a slice of bytes.
*/
type GophermapText struct {
contents []byte
Contents []byte
}
func NewGophermapText(contents []byte) *GophermapText {
s := new(GophermapText)
s.contents = contents
return s
return &GophermapText{ contents }
}
func (s *GophermapText) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return replaceStrings(string(s.contents), request.Host), nil
return replaceStrings(string(s.Contents), request.Host), nil
}
/* GophermapDirListing:
@ -113,21 +132,19 @@ func (s *GophermapText) Render(request *FileSystemRequest) ([]byte, *GophorError
* Render() call is received.
*/
type GophermapDirListing struct {
path string
Path string
Hidden map[string]bool
}
func NewGophermapDirListing(path string) *GophermapDirListing {
s := new(GophermapDirListing)
s.path = path
return s
return &GophermapDirListing{ path, nil }
}
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)
return listDir(&FileSystemRequest{ s.Path, request.Host }, s.Hidden)
}
func readGophermap(path string) ([]GophermapSection, *GophorError) {

@ -0,0 +1,326 @@
package main
import (
"os"
"sync"
"path"
"time"
"strings"
)
type FileType int
const (
/* Leads to some more concise code below */
FileTypeRegular FileType = iota
FileTypeDir FileType = iota
FileTypeBad FileType = iota
)
/* 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 {
CacheMap *FixedMap
CacheMutex sync.RWMutex
CacheFileMax int64
}
func (fs *FileSystem) Init(size int, fileSizeMax float64) {
fs.CacheMap = NewFixedMap(size)
fs.CacheMutex = sync.RWMutex{}
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
}
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 }
}
/* It's there! Get contents, unlock and return */
file.Mutex.RLock()
b := file.Contents(&FileSystemRequest{ requestPath, host })
file.Mutex.RUnlock()
fs.CacheMutex.RUnlock()
return b, nil
}
/* Set file type for later handling */
switch {
case stat.Mode() & os.ModeDir != 0:
/* do nothing, already set :) */
break
case stat.Mode() & os.ModeType == 0:
fileType = FileTypeRegular
default:
fileType = FileTypeBad
}
}
switch fileType {
/* Directory */
case FileTypeDir:
/* Check Gophermap exists */
gophermapPath := path.Join(requestPath, GophermapFileStr)
_, err := os.Stat(gophermapPath)
var output []byte
var gophorErr *GophorError
if err == nil {
/* Gophermap exists, serve this! */
output, gophorErr = fs.FetchFile(&FileSystemRequest{ gophermapPath, host })
} else {
/* No gophermap, serve directory listing */
output, gophorErr = listDir(&FileSystemRequest{ requestPath, host }, map[string]bool{})
}
if gophorErr != nil {
/* Fail out! */
return nil, gophorErr
}
/* Append footer text (contains last line) and return */
output = append(output, Config.FooterText...)
return output, nil
/* Regular file */
case FileTypeRegular:
return fs.FetchFile(&FileSystemRequest{ requestPath, host })
/* Unsupported type */
default:
return nil, &GophorError{ FileTypeErr, nil }
}
}
func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorError) {
/* Get cache map read lock then check if file in cache map */
fs.CacheMutex.RLock()
file := fs.CacheMap.Get(request.Path)
if file != nil {
/* File in cache -- before doing anything get file read lock */
file.Mutex.RLock()
/* Check file is marked as fresh */
if !file.Fresh {
/* File not fresh! Swap file read for write-lock */
file.Mutex.RUnlock()
file.Mutex.Lock()
/* Reload file contents from disk */
gophorErr := file.LoadContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
file.Mutex.Unlock()
fs.CacheMutex.RUnlock()
return nil, gophorErr
}
/* Updated! Swap back file write for read lock */
file.Mutex.Unlock()
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
*/
stat, err := os.Stat(request.Path)
if err != nil {
/* Error stat'ing file, unlock read mutex then return error */
fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
}
/* Create new file contents object using supplied function */
var contents FileContents
if strings.HasSuffix(request.Path, "/"+GophermapFileStr) {
contents = &GophermapContents{ request.Path, nil }
} else {
contents = &RegularFileContents{ request.Path, nil }
}
/* Create new file wrapper around contents */
file = NewFile(contents)
/* File isn't in cache yet so no need to get file lock mutex */
gophorErr := file.LoadContents()
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
}
/* File not in cache -- Swap cache map read for write lock. */
fs.CacheMutex.RUnlock()
fs.CacheMutex.Lock()
/* Put file in the FixedMap */
fs.CacheMap.Put(request.Path, file)
/* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */
file.Mutex.RLock()
/* Swap cache lock back to read */
fs.CacheMutex.Unlock()
fs.CacheMutex.RLock()
}
/* Read file contents into new variable for return, then unlock file read lock */
b := file.Contents(request)
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
}
/* 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
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) LoadContents() *GophorError {
/* Clear current file contents */
f.contents.Clear()
/* Reload the file */
gophorErr := f.contents.Load()
if gophorErr != nil {
return gophorErr
}
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.LastRefresh = time.Now().UnixNano()
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()
}
func startFileMonitor(sleepTime time.Duration) {
go func() {
for {
/* Sleep so we don't take up all the precious CPU time :) */
time.Sleep(sleepTime)
/* Check global file cache freshness */
checkCacheFreshness()
}
/* We shouldn't have reached here */
Config.LogSystemFatal("FileCache monitor escaped run loop!\n")
}()
}
func checkCacheFreshness() {
/* Before anything, get cache write lock (in case we have to delete) */
Config.FileSystem.CacheMutex.Lock()
/* Iterate through paths in cache map to query file last modified times */
for path := range Config.FileSystem.CacheMap.Map {
/* Get file pointer, no need for lock as we have write lock */
file := Config.FileSystem.CacheMap.Get(path)
/* If this is a generated file, we skip */
if isGeneratedType(file) {
continue
}
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.FileSystem.CacheMap.Remove(path)
continue
}
timeModified := stat.ModTime().UnixNano()
/* If the file is marked as fresh, but file on disk newer, mark as unfresh */
if file.Fresh && file.LastRefresh < timeModified {
file.Fresh = false
}
}
/* Done! We can release cache read lock */
Config.FileSystem.CacheMutex.Unlock()
}
func isGeneratedType(file *File) bool {
/* Just a helper function to neaten-up checking if file contents is of generated type */
switch file.contents.(type) {
case *GeneratedFileContents:
return true
default:
return false
}
}

@ -2,111 +2,13 @@ package main
import (
"os"
"sync"
"path"
"bytes"
"time"
"io"
"sort"
"bufio"
)
/* FileSystemRequest:
* Makes a request to the filesystem either through
* the FileCache or directly to a function like listDir().
* It carries the requested filesystem path and any extra
* needed information, for the moment just a set of details
* about the virtual host.. Opens things up a lot more for
* the future :)
*/
type FileSystemRequest struct {
Path string
Host *ConnHost
}
/* File:
* Wraps around the cached contents of a file and
* helps with management of this content by the
* global FileCache objects.
*/
type File struct {
contents FileContents
mutex sync.RWMutex
isFresh bool
lastRefresh int64
}
func NewFile(contents FileContents) *File {
f := new(File)
f.contents = contents
f.mutex = sync.RWMutex{}
f.isFresh = true
f.lastRefresh = 0
return f
}
func (f *File) Contents(request *FileSystemRequest) []byte {
return f.contents.Render(request)
}
func (f *File) LoadContents() *GophorError {
/* Clear current file contents */
f.contents.Clear()
/* Reload the file */
gophorErr := f.contents.Load()
if gophorErr != nil {
return gophorErr
}
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.lastRefresh = time.Now().UnixNano()
f.isFresh = true
return nil
}
func (f *File) IsFresh() bool {
return f.isFresh
}
func (f *File) SetUnfresh() {
f.isFresh = false
}
func (f *File) LastRefresh() int64 {
return f.lastRefresh
}
func (f *File) Lock() {
f.mutex.Lock()
}
func (f *File) Unlock() {
f.mutex.Unlock()
}
func (f *File) RLock() {
f.mutex.RLock()
}
func (f *File) RUnlock() {
f.mutex.RUnlock()
}
/* 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()
}
/* Perform simple buffered read on a file at path */
func bufferedRead(path string) ([]byte, *GophorError) {
/* Open file */

@ -27,11 +27,11 @@ type MapElement struct {
}
func NewFixedMap(size int) *FixedMap {
fm := new(FixedMap)
fm.Map = make(map[string]*MapElement)
fm.List = list.New()
fm.Size = size
return fm
return &FixedMap{
make(map[string]*MapElement),
list.New(),
size,
}
}
/* Get file in map for key, or nil */

@ -5,65 +5,80 @@ import (
)
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,
".vim": TypeFile,
".nanorc": 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,
".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 {
@ -126,6 +141,31 @@ func getItemType(name string) ItemType {
}
}
/* 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)
if text != "" {
ret = append(ret, buildInfoLine("")...)
if useSeparator {
ret = append(ret, buildInfoLine(buildLineSeparator(Config.PageWidth))...)
}
for _, line := range strings.Split(text, "\n") {
ret = append(ret, buildInfoLine(line)...)
}
}
ret = append(ret, []byte(LastLine)...)
return ret
}
/* Parse line type from contents */
func parseLineType(line string) ItemType {
lineLen := len(line)

@ -0,0 +1,193 @@
#!/bin/sh
PIDFILE='/tmp/gophor.pid'
GOPHOR='./gophor'
LOGDIR='/var/log/gophor'
BINDADDR=''
PORT=70
HOSTNAME=''
RUNUSER=''
ROOTDIR=''
CACHESIZE='100'
CACHEFILEMAXMB='1'
CACHECHECKFREQ='1s'
PAGEWIDTH=80
FOOTERTEXT='Running Gophor, a Gopher server in Go.'
usage() {
echo 'Usage: gophor-run start|stop|status'
}
start() {
local exec_flags
if [ "$LOGDIR" = '' ]; then
# If log dir not set, disable logging
exec_flags="$exec_flags -log-type 1"
else
# Add log file paths
exec_flags="$exec_flags -system-log "${LOGDIR}/system.log" -access-log "${LOGDIR}/access.log""
# If log dir doesn't exist, try make it!
if [ ! -d "$LOGDIR" ] && !(mkdir "$LOGDIR" > /dev/null 2>&1) && !(sudo mkdir "$LOGDIR" > /dev/null 2>&1); then
echo "Log file directory does not exist, and failed to create: $LOGDIR"
exit 1
fi
fi
# Add bind addr (no worries if empty)
if [ "$BINDADDR" != '' ]; then
exec_flags="$exec_flags -bind-addr "${BINDADDR}""
fi
# Add port, don't worry if not
if [ "$PORT" != '' ]; then
exec_flags="$exec_flags -port "${PORT}""
fi
# Try to add hostname
if [ "$HOSTNAME" != '' ]; then
exec_flags="$exec_flags -hostname "${HOSTNAME}""
else
echo 'HOSTNAME variable must not be empty!'
echo 'This is used to generate Gopher directory entries.'
exit 1
fi
# Add user to run under (no worries if empty)
if [ "$RUNUSER" != '' ]; then
exec_flags="$exec_flags -user "${RUNUSER}""
fi
# Add root dir (no worries if empty)
if [ "$ROOTDIR" != '' ]; then
exec_flags="$exec_flags -root "${ROOTDIR}""
fi
# Add page width, don't worry if not
if [ "$PAGEWIDTH" != '' ]; then
exec_flags="$exec_flags -page-width "${PAGEWIDTH}""
fi
# Add footer text (no worries if empty)
if [ "$FOOTER" != '' ]; then
exec_flags="$exec_flags -footer "${FOOTERTEXT}""
fi
# If cache size 0 or empty, disable cache. Else, set
if [ "$CACHESIZE" = '' ] || [ "$CACHESIZE" -eq 0 ]; then
exec_flags="$exec_flags -disable-cache"
else
exec_flags="$exec_flags -cache-size "${CACHESIZE}""
# Add file size max in megabytes (no worries if empty)
if [ "$CACHEFILEMAXMB" != '' ]; then
exec_flags="$exec_flags -cache-file-max "${CACHEFILEMAXMB}""
fi
# Add cache staleness check frequency (no worries if empty)
if [ "$CACHECHECKFREQ" != '' ]; then
exec_flags="$exec_flags -cache-check "${CACHECHECKFREQ}""
fi
fi
# If logfiles provided, in background. Else, front!
echo -n 'Gophor server starting...'
sudo "$GOPHOR" $exec_flags & > /dev/null 2>&1
local pid="$!"
sleep 1
if (sudo kill -0 "$pid" > /dev/null 2>&1); then
echo ' Successful!'
sudo sh -c "echo "$pid" > "$PIDFILE""
sudo chmod 0644 "$PIDFILE"
exit 0
else
echo ' Failed!'
exit 1
fi
}
stop() {
if [ ! -f "$PIDFILE" ]; then
if (ps -ax | grep -v 'grep' | grep -q 'openvpn'); then
echo 'Gophor is running, but no PID file exists. Was this started without gophor-run?'
else
echo 'Gophor is not running!'
fi
return 1
fi
if (sudo kill "$(cat "$PIDFILE")" > /dev/null 2>&1); then
echo 'Successfully stopped gophor'
sudo rm -f "$PIDFILE"
return 0
else
if ! (sudo kill -0 "$(cat "$PIDFILE")" > /dev/null 2>&1); then
echo 'Gophor not running!'
return 1
else
echo 'Unable to stop gophor process'
return 1
fi
fi
}
status() {
if [ -f "$PIDFILE" ]; then
if (sudo kill -0 "$(cat "$PIDFILE")" > /dev/null 2>&1); then
echo 'Gophor is running!'
return 0
else
echo 'Gophor is not running'
sudo rm -f "$PIDFILE"
return 1
fi
else
if (ps -ax | grep -v 'grep' | grep -q 'openvpn'); then
echo 'Gophor is running but not at expected PID. Was this started without gophor-run?'
return 1
else
echo 'Gophor is not running'
return 1
fi
fi
}
if [ $# -ne 1 ]; then
usage
exit 0
fi
if [ $(id -u) -eq 0 ]; then
echo 'Please do not run this script as root! Root permissions will be requested when necessary'
exit 1
fi
echo 'This is an example script to ease automation of running gophor.'
echo 'Not recommended for use in production environments.'
echo ''
case "$1" in
'start')
start
;;
'stop')
stop
;;
'status')
status
;;
*)
usage
;;
esac

@ -43,6 +43,8 @@ func main() {
/* Start accepting connections on any supplied listeners */
for _, l := range listeners {
go func() {
Config.LogSystem("Listening on: gopher://%s\n", l.Addr())
for {
newConn, err := l.Accept()
if err != nil {
@ -52,8 +54,7 @@ func main() {
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
w := NewWorker(newConn)
w.Serve()
NewWorker(newConn).Serve()
}()
}
}()
@ -76,11 +77,14 @@ func setupServer() []*GophorListener {
execAs := flag.String("user", "", "Drop to supplied user's UID and GID permissions before execution.")
/* User supplied caps.txt information */
serverDescription := flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in auto-generated caps.txt.")
serverAdmin := flag.String("admin-email", "", "Change admin email in auto-generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in auto-generated caps.txt.")
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.")
/* 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.")
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting files from showing in directory listings.")
@ -93,18 +97,25 @@ func setupServer() []*GophorListener {
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.")
/* Version string */
version := flag.Bool("version", false, "Print version information.")
/* Parse parse parse!! */
flag.Parse()
if *version {
printVersionExit()
}
/* Setup the server configuration instance and enter as much as we can right now */
Config = new(ServerConfig)
Config.RootDir = *serverRoot
Config.Description = *serverDescription
Config.AdminEmail = *serverAdmin
Config.Geolocation = *serverGeoloc
Config.PageWidth = *pageWidth
/* Have to be set AFTER page width variable set */
Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator)
/* Setup Gophor logging system */
Config.SystemLogger, Config.AccessLogger = setupLogging(*logType, *systemLogPath, *accessLogPath)
@ -146,7 +157,6 @@ func setupServer() []*GophorListener {
if err != nil {
Config.LogSystemFatal("Error setting up (unencrypted) listener: %s\n", err.Error())
}
Config.LogSystem("Listening (unencrypted): gopher://%s\n", l.Addr())
listeners = append(listeners, l)
} else {
Config.LogSystemFatal("No valid port to listen on :(\n")
@ -167,18 +177,36 @@ func setupServer() []*GophorListener {
listDir = _listDir
}
/* 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)
}
/* Setup file cache */
Config.FileCache = new(FileCache)
Config.FileCache.Init(*cacheSize, *cacheFileSizeMax)
Config.FileSystem = new(FileSystem)
/* Start file cache freshness checker */
go startFileMonitor(fileMonitorSleepTime)
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)
/* Start file cache freshness checker */
go startFileMonitor(fileMonitorSleepTime)
Config.LogSystem("File cache freshness monitor started with frequency: %s\n", fileMonitorSleepTime)
} else {
/* File caching disabled, init with zero max size so nothing gets cached */
Config.FileSystem.Init(2, 0)
Config.LogSystem("File caching disabled\n")
/* Safe to cache policy files now */
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc)
}
/* Return the created listeners slice :) */
return listeners

@ -63,3 +63,10 @@ func setupLogging(loggingType int, systemLogPath, accessLogPath string) (*log.Lo
return systemLogger, accessLogger
}
func printVersionExit() {
/* Reset the flags before printing version */
log.SetFlags(0)
log.Printf("%s\n", GophorVersion)
os.Exit(0)
}

@ -1,6 +1,50 @@
package main
func generateCapsTxt() []byte {
import (
"os"
)
func cachePolicyFiles(description, admin, geoloc string) {
/* See if caps txt exists, if not generate */
_, err := os.Stat("/caps.txt")
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)
/* Trigger a load contents just to set it as fresh etc */
file.LoadContents()
/* 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")
}
/* See if caps txt exists, if not generate */
_, err = os.Stat("/robots.txt")
if err != nil {
/* We need to generate the caps txt and manually load into cache */
content := generateRobotsTxt()
/* Create new file object from generated file contents */
fileContents := &GeneratedFileContents{ content }
file := NewFile(fileContents)
/* Trigger a load contents just to set it as fresh etc */
file.LoadContents()
/* 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")
}
}
func generateCapsTxt(description, admin, geoloc string) []byte {
text := "CAPS"+DOSLineEnd
text += DOSLineEnd
text += "# This is an automatically generated"+DOSLineEnd
@ -18,11 +62,11 @@ func generateCapsTxt() []byte {
text += DOSLineEnd
text += "ServerSoftware=Gophor"+DOSLineEnd
text += "ServerSoftwareVersion="+GophorVersion+DOSLineEnd
text += "ServerDescription="+Config.Description+DOSLineEnd
text += "ServerGeolocationString="+Config.Geolocation+DOSLineEnd
text += "ServerDescription="+description+DOSLineEnd
text += "ServerGeolocationString="+geoloc+DOSLineEnd
text += "ServerDefaultEncoding=ascii"+DOSLineEnd
text += DOSLineEnd
text += "ServerAdmin="+Config.AdminEmail+DOSLineEnd
text += "ServerAdmin="+admin+DOSLineEnd
return []byte(text)
}

@ -1,92 +1,75 @@
package main
import (
"os"
"path"
"strings"
)
type FileType int
const (
SocketReadBufSize = 256 /* Supplied selector shouldn't be longer than this anyways */
MaxSocketReadChunks = 4
FileReadBufSize = 1024
/* Leads to some more concise code below */
FileTypeRegular FileType = iota
FileTypeDir FileType = iota
FileTypeBad FileType = iota
)
type Worker struct {
Conn *GophorConn
}
func NewWorker(conn *GophorConn) *Worker {
worker := new(Worker)
worker.Conn = conn
return worker
return &Worker{ conn }
}
func (worker *Worker) Serve() {
go func() {
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
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
}
/* Handle request */
gophorErr := worker.RespondGopher(received)
/* Only copy non-null bytes */
received = append(received, buf[:count]...)
/* Handle any error */
if gophorErr != nil {
Config.LogSystemError("%s\n", gophorErr.Error())
/* If count is less than expected read size, we've hit EOF */
if count < SocketReadBufSize {
/* EOF */
break
}
/* Generate response bytes from error code */
response := generateGopherErrorResponseFromCode(gophorErr.Code)
/* 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
}
/* 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)
}
/* Keep count :) */
iter += 1
}
/* Handle request */
gophorErr := worker.RespondGopher(received)
/* Handle any error */
if gophorErr != nil {
Config.LogSystemError("%s\n", gophorErr.Error())
/* Generate response bytes from error code */
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 {
@ -111,13 +94,13 @@ func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* According to Gopher spec, only read up to first Tab or Crlf */
dataStr := readUpToFirstTabOrCrlf(data)
/* Handle URL request if so. TODO: this is so unelegant... */
/* Handle URL request if presented */
lenBefore := len(dataStr)
dataStr = strings.TrimPrefix(dataStr, "URL:")
switch len(dataStr) {
case lenBefore-4:
/* Handle URL prefix */
worker.Log("Redirecting to URL: %s\n", data)
/* Send an HTML redirect to supplied URL */
worker.Log("Redirecting to %s\n", dataStr)
return worker.SendRaw(generateHtmlRedirect(dataStr))
default:
/* Do nothing */
@ -126,90 +109,13 @@ func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* Sanitize supplied path */
requestPath := sanitizePath(dataStr)
/* Handle policy files. TODO: this is so unelegant... */
switch requestPath {
case "/"+CapsTxtStr:
return worker.SendRaw(generateCapsTxt())
case "/"+RobotsTxtStr:
return worker.SendRaw(generateRobotsTxt())
}
/* Open requestPath */
file, err := os.Open(requestPath)
if err != nil {
return &GophorError{ FileOpenErr, err }
}
/* If not empty requestPath, check file type.
* Default type is directory.
*/
fileType := FileTypeDir
if requestPath != "." {
stat, err := file.Stat()
if err != nil {
return &GophorError{ FileStatErr, err }
}
switch {
case stat.Mode() & os.ModeDir != 0:
// do nothing :)
case stat.Mode() & os.ModeType == 0:
fileType = FileTypeRegular
default:
fileType = FileTypeBad
}
}
/* Don't need the file handle anymore */
file.Close()
/* Handle file type. TODO: work on efficiency */
response := make([]byte, 0)
switch fileType {
/* Directory */
case FileTypeDir:
/* First try to serve gopher map */
gophermapPath := path.Join(requestPath, "/"+GophermapFileStr)
fileContents, gophorErr := Config.FileCache.FetchGophermap(&FileSystemRequest{ gophermapPath, worker.Conn.Host })
if gophorErr != nil {
/* Get directory listing instead */
fileContents, gophorErr = listDir(&FileSystemRequest{ requestPath, worker.Conn.Host }, map[string]bool{})
if gophorErr != nil {
return gophorErr
}
/* Add fileContents to response */
response = append(response, fileContents...)
worker.Log("serve dir: %s\n", requestPath)
/* Finish directory listing with LastLine */
response = append(response, []byte(LastLine)...)
} else {
/* Successfully loaded gophermap, add fileContents to response */
response = append(response, fileContents...)
worker.Log("serve gophermap: %s\n", gophermapPath)
}
/* Regular file */
case FileTypeRegular:
/* Read file contents */
fileContents, gophorErr := Config.FileCache.FetchRegular(&FileSystemRequest{ requestPath, worker.Conn.Host })
if gophorErr != nil {
return gophorErr
}
/* Append fileContents to response */
response = append(response, fileContents...)
worker.Log("serve file: %s\n", requestPath)
/* Unsupport file type */
default:
return &GophorError{ FileTypeErr, nil }
}
/* Append lastline */
response = append(response, []byte(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)
/* Serve response */
return worker.SendRaw(response)
@ -220,16 +126,16 @@ func readUpToFirstTabOrCrlf(data []byte) string {
dataStr := ""
dataLen := len(data)
for i := 0; i < dataLen; i += 1 {
if data[i] == '\t' {
break
} else if data[i] == DOSLineEnd[0] {
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
/* Finished on Unix line end, NOT DOS */
break
}
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])
}
dataStr += string(data[i])
}
return dataStr

Loading…
Cancel
Save