gophor rewrite

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
master
kim (grufwub) 4 years ago
parent ebe414d8f3
commit 75cfbc0c65

@ -2,14 +2,45 @@
set -e
PROJECT='gophor'
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.*$||')"
PROJECT='gophor.gopher'
#VERSION="$(cat 'gophor.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')"
VERSION='v0.02-alpha'
LOGFILE='build.log'
OUTDIR="build-${VERSION}"
OUTDIR="build-gopher-${VERSION}"
silent() {
"$@" >> "$LOGFILE" 2>&1
upx_compress() {
local level="$1" filename="$2" topack="${2}.topack"
cp "$filename" "$topack"
if (upx "$level" "$topack" >> "$LOGFILE" 2>&1); then
if (upx --test "$topack"); then
mv "$topack" "$filename"
return 0
else
rm "$topack"
return 1
fi
else
rm "$topack"
return 1
fi
}
compress() {
local filename="$1"
echo "Attempting to compress ${filename}..."
if (upx_compress '--ultra-brute' "$filename"); then
echo "Compressed with --ultra-brute!"
elif (upx_compress '--best' "$filename"); then
echo "Compressed with --best!"
elif (upx_compress '' "$filename"); then
echo "Compressed with no flags."
else
echo "Compression failed!"
fi
}
build_for() {
@ -20,34 +51,15 @@ build_for() {
shift 1
fi
echo "Building for ${os} ${archname}..."
echo "Building for ${os} ${archname} with ${toolchain}..."
local filename="${OUTDIR}/${PROJECT}_${os}_${archname}"
CGO_ENABLED=1 CC="$toolchain-gcc" GOOS="$os" GOARCH="$arch" GOARM="$armversion" silent go build -trimpath -o "$filename" "$@"
CC="${toolchain}-gcc" CGO_ENABLED=1 GOOS="$os" GOARCH="$arch" GOARM="$armversion" go build -trimpath -o "$filename" "$@" 'main_gopher.go' >> "$LOGFILE" 2>&1
if [ "$?" -ne 0 ]; then
echo "Failed!"
return 1
fi
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 ""
compress "$filename"
}
echo "PLEASE BE WARNED THIS SCRIPT IS WRITTEN FOR A VOID LINUX (MUSL) BUILD ENVIRONMENT"

@ -0,0 +1,3 @@
#!/bin/sh
CC='x86_64-linux-musl-gcc' CGO_ENABLED=1 go build -trimpath -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"' -o gophor.gopher main_gopher.go

@ -1,46 +0,0 @@
package main
import (
"time"
"regexp"
)
/* ServerConfig:
* Holds onto global server configuration details
* and any data objects we want to keep in memory
* (e.g. loggers, restricted files regular expressions
* and file cache)
*/
type ServerConfig struct {
/* Executable Settings */
Env []string
CgiEnv []string
CgiEnabled bool
MaxExecRunTime time.Duration
/* Content settings */
FooterText []byte
PageWidth int
/* Logging */
SysLog LoggerInterface
AccLog LoggerInterface
/* Filesystem access */
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,175 +0,0 @@
package main
import (
"io"
"net"
"time"
"bufio"
"strconv"
)
type ConnHost struct {
/* 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()
}
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, fwdPort, rootDir string) (*GophorListener, error) {
gophorListener := new(GophorListener)
gophorListener.Host = &ConnHost{ hostname, port, fwdPort }
gophorListener.Root = rootDir
var err error
gophorListener.Listener, err = net.Listen("tcp", bindAddr+":"+port)
if err != nil {
return nil, err
} else {
return gophorListener, nil
}
}
func (l *GophorListener) Accept() (*Worker, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
/* 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
}
type DeadlineConn struct {
/* Simple wrapper to net.Conn that sets deadlines
* on each call to Read() / Write()
*/
conn net.Conn
}
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 *BufferedDeadlineConn) Write(b []byte) (int, error) {
return c.buffer.Write(b)
}
func (c *BufferedDeadlineConn) WriteData(b []byte) error {
_, err := c.buffer.Write(b)
return err
}
func (c *BufferedDeadlineConn) WriteRaw(r io.Reader) error {
_, err := c.buffer.ReadFrom(r)
return err
}
func (c *BufferedDeadlineConn) Close() error {
/* First flush buffer, then close */
c.buffer.Flush()
return c.conn.Close()
}

@ -0,0 +1,77 @@
package core
import "container/list"
// element wraps a map key and value
type element struct {
key string
value *file
}
// lruCacheMap is a fixed-size LRU hash map
type lruCacheMap struct {
hashMap map[string]*list.Element
list *list.List
size int
}
// newLRUCacheMap returns a new LRUCacheMap of specified size
func newLRUCacheMap(size int) *lruCacheMap {
return &lruCacheMap{
// size+1 to account for moment during put after adding new value but before old value is purged
make(map[string]*list.Element, size+1),
&list.List{},
size,
}
}
// Get returns file from LRUCacheMap for key
func (lru *lruCacheMap) Get(key string) (*file, bool) {
lElem, ok := lru.hashMap[key]
if !ok {
return nil, ok
}
// Move element to front of the list
lru.list.MoveToFront(lElem)
// Get Element and return *File value from it
element, _ := lElem.Value.(*element)
return element.value, ok
}
// Put file in LRUCacheMap at key
func (lru *lruCacheMap) Put(key string, value *file) {
lElem := lru.list.PushFront(&element{key, value})
lru.hashMap[key] = lElem
if lru.list.Len() > lru.size {
// Get element at back of list and Element from it
lElem = lru.list.Back()
element, _ := lElem.Value.(*element)
// Delete entry in hashMap with key from Element, and from list
delete(lru.hashMap, element.key)
lru.list.Remove(lElem)
}
}
// Remove file in LRUCacheMap with key
func (lru *lruCacheMap) Remove(key string) {
lElem, ok := lru.hashMap[key]
if !ok {
return
}
// Delete entry in hashMap and list
delete(lru.hashMap, key)
lru.list.Remove(lElem)
}
// Iterate performs an iteration over all key:value pairs in LRUCacheMap with supplied function
func (lru *lruCacheMap) Iterate(iterator func(key string, value *file)) {
for key := range lru.hashMap {
element, _ := lru.hashMap[key].Value.(*element)
iterator(element.key, element.value)
}
}

@ -0,0 +1,331 @@
package core
import (
"bytes"
"io"
"os/exec"
"strings"
"syscall"
"time"
)
var (
// cgiEnv holds the global slice of constant CGI environment variables
cgiEnv []string
// maxCGIRunTime specifies the maximum time a CGI script can run for
maxCGIRunTime time.Duration
// httpPrefixBufSize specifies size of the buffer to use when skipping HTTP headers
httpPrefixBufSize int
// ExecuteCGIScript is a pointer to the currently set CGI execution function
ExecuteCGIScript func(*Client, *Request) Error
)
// setupInitialCGIEnv takes a safe PATH, uses other server variables and returns a slice of constant CGI environment variables
func setupInitialCGIEnv(safePath string) []string {
env := make([]string, 0)
SystemLog.Info("CGI safe path: %s", safePath)
env = append(env, "PATH="+safePath)
env = append(env, "SERVER_NAME="+Hostname)
env = append(env, "SERVER_PORT="+FwdPort)
env = append(env, "DOCUMENT_ROOT="+Root)
return env
}
// generateCGIEnv takes a Client, and Request object, the global constant slice and generates a full set of CGI environment variables
func generateCGIEnv(client *Client, request *Request) []string {
env := append(cgiEnv, "REMOTE_ADDR="+client.IP())
env = append(env, "QUERY_STRING="+request.Params())
env = append(env, "SCRIPT_NAME="+request.Path().Relative())
env = append(env, "SCRIPT_FILENAME="+request.Path().Absolute())
env = append(env, "SELECTOR="+request.Path().Selector())
env = append(env, "REQUEST_URI="+request.Path().Selector())
return env
}
// executeCGIScriptNoHTTP executes a CGI script, responding with output to client without stripping HTTP headers
func executeCGIScriptNoHTTP(client *Client, request *Request) Error {
return execute(client.Conn().Writer(), request.Path(), generateCGIEnv(client, request))
}
// executeCGIScriptStripHTTP executes a CGI script, responding with output to client, stripping HTTP headers and handling status code
func executeCGIScriptStripHTTP(client *Client, request *Request) Error {
// Create new httpStripWriter
httpWriter := newhttpStripWriter(client.Conn().Writer())
// Begin executing script
err := execute(httpWriter, request.Path(), generateCGIEnv(client, request))
// Parse HTTP headers (if present). Return error or continue letting output of script -> client
cgiStatusErr := httpWriter.FinishUp()
if cgiStatusErr != nil {
return cgiStatusErr
}
return err
}
// execute executes something at Path, with supplied environment and ouputing to writer
func execute(writer io.Writer, p *Path, env []string) Error {
// Create cmd object
cmd := exec.Command(p.Absolute())
// Set new process group id
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
// Setup cmd environment
cmd.Env, cmd.Dir = env, p.Root()
// Setup cmd out writer
cmd.Stdout = writer
// Start executing
err := cmd.Start()
if err != nil {
return WrapError(CGIStartErr, err)
}
// Setup goroutine to kill cmd after maxCGIRunTime
go func() {
// At least let the script try to finish...
time.Sleep(maxCGIRunTime)
// We've already finished
if cmd.ProcessState != nil {
return
}
// Get process group id
pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err != nil {
SystemLog.Fatal(pgidNotFoundErrStr)
}
// Kill process group!
err = syscall.Kill(-pgid, syscall.SIGTERM)
if err != nil {
SystemLog.Fatal(pgidStopErrStr, 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 directly from command process state
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
}
// Non-zero exit code? Return error
if exitCode != 0 {
SystemLog.Error(cgiExecuteErrStr, p.Absolute(), exitCode)
return NewError(CGIExitCodeErr)
}
// Exit fine!
return nil
}
// httpStripWriter wraps a writer, reading HTTP headers and parsing status code, before deciding to continue writing
type httpStripWriter struct {
writer io.Writer
skipBuffer []byte
skipIndex int
err Error
// writeFunc is a pointer to the current underlying write function
writeFunc func(*httpStripWriter, []byte) (int, error)
}
// newhttpStripWriter returns a new httpStripWriter wrapping supplied writer
func newhttpStripWriter(w io.Writer) *httpStripWriter {
return &httpStripWriter{
w,
make([]byte, httpPrefixBufSize),
0,
nil,
writeCheckForHeaders,
}
}
// addToSkipBuffer adds supplied bytes to the skip buffer, returning number added
func (w *httpStripWriter) addToSkipBuffer(data []byte) int {
// Figure out amount to add
toAdd := len(w.skipBuffer) - w.skipIndex
if len(data) < toAdd {
toAdd = len(data)
}
// Add data to skip buffer, return added
copy(w.skipBuffer[w.skipIndex:], data[:toAdd])
w.skipIndex += toAdd
return toAdd
}
// parseHTTPHeaderSection checks if we've received a valid HTTP header section, and determine if we should continue writing
func (w *httpStripWriter) parseHTTPHeaderSection() (bool, bool) {
validHeaderSection, shouldContinue := false, true
for _, header := range strings.Split(string(w.skipBuffer), "\r\n") {
header = strings.ToLower(header)
// Try look for status header
lenBefore := len(header)
header = strings.TrimPrefix(header, "status:")
if len(header) < lenBefore {
// Ensure no spaces + just number
header = strings.Split(header, " ")[0]
// Ignore 200
if header == "200" {
continue
}
// Any other value indicates error, should not continue
shouldContinue = false
// Parse error code
code := CGIStatusUnknownErr
switch header {
case "400":
code = CGIStatus400Err
case "401":
code = CGIStatus401Err
case "403":
code = CGIStatus403Err
case "404":
code = CGIStatus404Err
case "408":
code = CGIStatus408Err
case "410":
code = CGIStatus410Err
case "500":
code = CGIStatus500Err
case "501":
code = CGIStatus501Err
case "503":
code = CGIStatus503Err
}
// Set error code
w.err = NewError(code)
continue
}
// Found a content-type header, this is a valid header section
if strings.Contains(header, "content-type:") {
validHeaderSection = true
}
}
return validHeaderSection, shouldContinue
}
// writeSkipBuffer writes contents of skipBuffer to the underlying writer if necessary
func (w *httpStripWriter) writeSkipBuffer() (bool, error) {
// Defer resetting skipIndex
defer func() {
w.skipIndex = 0
}()
// First try parse the headers, determine next steps
validHeaders, shouldContinue := w.parseHTTPHeaderSection()
// Valid headers received, don't bother writing. Return the shouldContinue value
if validHeaders {
return shouldContinue, nil
}
// Default is to write skip buffer contents, shouldContinue only means something with valid headers
_, err := w.writer.Write(w.skipBuffer[:w.skipIndex])
return true, err
}
func (w *httpStripWriter) FinishUp() Error {
// If skipIndex not zero, try write (or at least parse and see if we need
// to write) remaining skipBuffer. (e.g. if CGI output very short)
if w.skipIndex > 0 {
w.writeSkipBuffer()
}
// Return error if set
return w.err
}
func (w *httpStripWriter) Write(b []byte) (int, error) {
// Write using currently set write function
return w.writeFunc(w, b)
}
// writeRegular performs task of regular write function, it is a direct wrapper
func writeRegular(w *httpStripWriter, b []byte) (int, error) {
return w.writer.Write(b)
}
// writeCheckForHeaders reads input data, checking for headers to add to skip buffer and parse before continuing
func writeCheckForHeaders(w *httpStripWriter, b []byte) (int, error) {
split := bytes.Split(b, []byte("\r\n\r\n"))
if len(split) == 1 {
// Headers found, try to add data to skip buffer
added := w.addToSkipBuffer(b)
if added < len(b) {
defer func() {
// Having written skip buffer, defer resetting write function
w.writeFunc = writeRegular
}()
doContinue, err := w.writeSkipBuffer()
if !doContinue {
return len(b), io.EOF
} else if err != nil {
return added, err
}
// Write remaining data not added to skip buffer
count, err := w.writer.Write(b[added:])
if err != nil {
return added + count, err
}
}
return len(b), nil
}
defer func() {
// No use for skip buffer after belo, set write to regular
w.writeFunc = writeRegular
}()
// Try add what we can to skip buffer
added := w.addToSkipBuffer(append(split[0], []byte("\r\n\r\n")...))
// Write skip buffer data if necessary, check if we should continue
doContinue, err := w.writeSkipBuffer()
if !doContinue {
return len(b), io.EOF
} else if err != nil {
return added, err
}
// Write remaining data not added to skip buffer, to writer
count, err := w.writer.Write(b[added:])
if err != nil {
return added + count, err
}
return len(b), nil
}

@ -0,0 +1,45 @@
package core
import (
"net"
"strconv"
)
// Client holds onto an open Conn to a client, along with connection information
type Client struct {
cn *conn
ip *net.IP
port string
}
// NewClient returns a new client based on supplied net.TCPConn
func NewClient(conn *net.TCPConn) *Client {
addr, _ := conn.RemoteAddr().(*net.TCPAddr)
ip, port := &addr.IP, strconv.Itoa(addr.Port)
return &Client{wrapConn(conn), ip, port}
}
// Conn returns the underlying conn
func (c *Client) Conn() *conn {
return c.cn
}
// IP returns the client's IP string
func (c *Client) IP() string {
return c.ip.String()
}
// Port returns the client's connected port
func (c *Client) Port() string {
return c.port
}
// LogInfo logs to the global access logger with the client IP as a prefix
func (c *Client) LogInfo(fmt string, args ...interface{}) {
AccessLog.Info("("+c.ip.String()+") "+fmt, args...)
}
// LogError logs to the global access logger with the client IP as a prefix
func (c *Client) LogError(fmt string, args ...interface{}) {
AccessLog.Error("("+c.ip.String()+") "+fmt, args...)
}

@ -0,0 +1,120 @@
package core
import (
"bufio"
"io"
"net"
"time"
)
var (
// connReadDeadline specifies the connection read deadline
connReadDeadline time.Duration
// connWriteDeadline specifies the connection write deadline
connWriteDeadline time.Duration
// connReadBufSize specifies the connection read buffer size
connReadBufSize int
// connWriteBufSize specifies the connection write buffer size
connWriteBufSize int
// connReadMax specifies the connection read max (in bytes)
connReadMax int
)
// deadlineConn wraps net.Conn to set the read / write deadlines on each access
type deadlineConn struct {
conn net.Conn
}
// Read wraps the underlying net.Conn read function, setting read deadline on each access
func (c *deadlineConn) Read(b []byte) (int, error) {
c.conn.SetReadDeadline(time.Now().Add(connReadDeadline))
return c.conn.Read(b)
}
// Read wraps the underlying net.Conn write function, setting write deadline on each access
func (c *deadlineConn) Write(b []byte) (int, error) {
c.conn.SetWriteDeadline(time.Now().Add(connWriteDeadline))
return c.conn.Write(b)
}
// Close directly wraps underlying net.Conn close function
func (c *deadlineConn) Close() error {
return c.conn.Close()
}
// Conn wraps a DeadlineConn with a buffer
type conn struct {
buf *bufio.ReadWriter
closer io.Closer
}
// wrapConn wraps a net.Conn in DeadlineConn, then within Conn and returns the result
func wrapConn(c net.Conn) *conn {
deadlineConn := &deadlineConn{c}
buf := bufio.NewReadWriter(
bufio.NewReaderSize(deadlineConn, connReadBufSize),
bufio.NewWriterSize(deadlineConn, connWriteBufSize),
)
return &conn{buf, deadlineConn}
}
// ReadLine reads a single line and returns the result, or nil and error
func (c *conn) ReadLine() ([]byte, Error) {
// return slice
b := make([]byte, 0)
for len(b) < connReadMax {
// read the line
line, isPrefix, err := c.buf.ReadLine()
if err != nil {
return nil, WrapError(ConnReadErr, err)
}
// append line contents to return slice
b = append(b, line...)
// if finished reading, break out
if !isPrefix {
break
}
}
return b, nil
}
// WriteBytes writes a byte slice to the buffer and returns error status
func (c *conn) WriteBytes(b []byte) Error {
_, err := c.buf.Write(b)
if err != nil {
return WrapError(ConnWriteErr, err)
}
return nil
}
// WriteFrom writes to the buffer from a reader and returns error status
func (c *conn) WriteFrom(r io.Reader) Error {
_, err := c.buf.ReadFrom(r)
if err != nil {
return WrapError(ConnWriteErr, err)
}
return nil
}
// Writer returns the underlying buffer wrapped conn writer
func (c *conn) Writer() io.Writer {
return c.buf.Writer
}
// Close flushes the underlying buffer then closes the conn
func (c *conn) Close() Error {
err := c.buf.Flush()
err = c.closer.Close()
if err != nil {
return WrapError(ConnCloseErr, err)
}
return nil
}

@ -0,0 +1,147 @@
package core
// ErrorCode specifies types of errors for later identification
type ErrorCode int
// Core ErrorCodes
const (
ConnWriteErr ErrorCode = -1
ConnReadErr ErrorCode = -2
ConnCloseErr ErrorCode = -3
ListenerResolveErr ErrorCode = -4
ListenerBeginErr ErrorCode = -5
ListenerAcceptErr ErrorCode = -6
InvalidIPErr ErrorCode = -7
InvalidPortErr ErrorCode = -8
FileOpenErr ErrorCode = -9
FileStatErr ErrorCode = -10
FileReadErr ErrorCode = -11
FileTypeErr ErrorCode = -12
DirectoryReadErr ErrorCode = -13
RestrictedPathErr ErrorCode = -14
InvalidRequestErr ErrorCode = -15
CGIStartErr ErrorCode = -16
CGIExitCodeErr ErrorCode = -17
CGIStatus400Err ErrorCode = -18
CGIStatus401Err ErrorCode = -19
CGIStatus403Err ErrorCode = -20
CGIStatus404Err ErrorCode = -21
CGIStatus408Err ErrorCode = -22
CGIStatus410Err ErrorCode = -23
CGIStatus500Err ErrorCode = -24
CGIStatus501Err ErrorCode = -25
CGIStatus503Err ErrorCode = -26
CGIStatusUnknownErr ErrorCode = -27
)
// Error specifies error interface with identifiable ErrorCode
type Error interface {
Code() ErrorCode
Error() string
}
// getExtendedErrorMessage converts an ErrorCode to string message
var getExtendedErrorMessage func(ErrorCode) string
// getErrorMessage converts an ErrorCode to string message first checking internal codes, next user supplied
func getErrorMessage(code ErrorCode) string {
switch code {
case ConnWriteErr:
return connWriteErrStr
case ConnReadErr:
return connReadErrStr
case ConnCloseErr:
return connCloseErrStr
case ListenerResolveErr:
return listenerResolveErrStr
case ListenerBeginErr:
return listenerBeginErrStr
case ListenerAcceptErr:
return listenerAcceptErrStr
case InvalidIPErr:
return invalidIPErrStr
case InvalidPortErr:
return invalidPortErrStr
case FileOpenErr:
return fileOpenErrStr
case FileStatErr:
return fileStatErrStr
case FileReadErr:
return fileReadErrStr
case FileTypeErr:
return fileTypeErrStr
case DirectoryReadErr:
return directoryReadErrStr
case RestrictedPathErr:
return restrictedPathErrStr
case InvalidRequestErr:
return invalidRequestErrStr
case CGIStartErr:
return cgiStartErrStr
case CGIExitCodeErr:
return cgiExitCodeErrStr
case CGIStatus400Err:
return cgiStatus400ErrStr
case CGIStatus401Err:
return cgiStatus401ErrStr
case CGIStatus403Err:
return cgiStatus403ErrStr
case CGIStatus404Err:
return cgiStatus404ErrStr
case CGIStatus408Err:
return cgiStatus408ErrStr
case CGIStatus410Err:
return cgiStatus410ErrStr
case CGIStatus500Err:
return cgiStatus500ErrStr
case CGIStatus501Err:
return cgiStatus501ErrStr
case CGIStatus503Err:
return cgiStatus503ErrStr
case CGIStatusUnknownErr:
return cgiStatusUnknownErrStr
default:
return getExtendedErrorMessage(code)
}
}
// regularError simply holds an ErrorCode
type regularError struct {
code ErrorCode
}
// Error returns the error string for the underlying ErrorCode
func (e *regularError) Error() string {
return getErrorMessage(e.code)
}
// Code returns the underlying ErrorCode
func (e *regularError) Code() ErrorCode {
return e.code
}
// NewError returns a new Error based on supplied ErrorCode
func NewError(code ErrorCode) Error {
return &regularError{code}
}
// wrappedError wraps an existing error with new ErrorCode
type wrappedError struct {
code ErrorCode
err error
}
// Error returns the error string for underlying error and set ErrorCode
func (e *wrappedError) Error() string {
return getErrorMessage(e.code) + " - " + e.err.Error()
}
// Code returns the underlying ErrorCode
func (e *wrappedError) Code() ErrorCode {
return e.code
}
// WrapError returns a new Error based on supplied error and ErrorCode
func WrapError(code ErrorCode, err error) Error {
return &wrappedError{code, err}
}

@ -0,0 +1,81 @@
package core
import (
"os"
"sync"
"time"
)
// isGeneratedType just checks if a file's contents implemented is GeneratedFileContents
func isGeneratedType(f *file) bool {
switch f.contents.(type) {
case *generatedFileContents:
return true
default:
return false
}
}
// file provides a structure for managing a cached file including freshness, last refresh time etc
type file struct {
contents FileContents
lastRefresh int64
isFresh bool
sync.RWMutex
}
// newFile returns a new File based on supplied FileContents
func newFile(contents FileContents) *file {
return &file{
contents,
0,
true,
sync.RWMutex{},
}
}
// IsFresh returns files freshness status
func (f *file) IsFresh() bool {
return f.isFresh
}
// SetFresh sets the file as fresh
func (f *file) SetFresh() {
f.isFresh = true
}
// SetUnfresh sets the file as unfresh
func (f *file) SetUnfresh() {
f.isFresh = false
}
// LastRefresh gets the time in nanoseconds of last refresh
func (f *file) LastRefresh() int64 {
return f.lastRefresh
}
// UpdateRefreshTime updates the lastRefresh time to the current time in nanoseconds
func (f *file) UpdateRefreshTime() {
f.lastRefresh = time.Now().UnixNano()
}
// CacheContents caches the file contents using the supplied file descriptor
func (f *file) CacheContents(fd *os.File, path *Path) Error {
f.contents.Clear()
// Load the file contents into cache
err := f.contents.Load(fd, path)
if err != nil {
return err
}
// Set the cache freshness
f.UpdateRefreshTime()
f.SetFresh()
return nil
}
// WriteToClient writes the cached file contents to the supplied client
func (f *file) WriteToClient(client *Client, path *Path) Error {
return f.contents.WriteToClient(client, path)
}

@ -0,0 +1,48 @@
package core
import "os"
// FileContents provides an interface for caching, rendering and getting cached contents of a file
type FileContents interface {
WriteToClient(*Client, *Path) Error
Load(*os.File, *Path) Error
Clear()
}
// generatedFileContents is a simple FileContents implementation for holding onto a generated (virtual) file contents
type generatedFileContents struct {
content []byte
}
// WriteToClient writes the generated file contents to the client
func (fc *generatedFileContents) WriteToClient(client *Client, path *Path) Error {
return client.Conn().WriteBytes(fc.content)
}
// Load does nothing
func (fc *generatedFileContents) Load(fd *os.File, path *Path) Error { return nil }
// Clear does nothing
func (fc *generatedFileContents) Clear() {}
// RegularFileContents is the simplest implementation of core.FileContents for regular files
type RegularFileContents struct {
contents []byte
}
// WriteToClient writes the current contents of FileContents to the client
func (fc *RegularFileContents) WriteToClient(client *Client, path *Path) Error {
return client.Conn().WriteBytes(fc.contents)
}
// Load takes an open FD and loads the file contents into FileContents memory
func (fc *RegularFileContents) Load(fd *os.File, path *Path) Error {
var err Error
fc.contents, err = FileSystem.ReadFile(fd)
return err
}
// Clear empties currently cached FileContents memory
func (fc *RegularFileContents) Clear() {
fc.contents = nil
}

@ -0,0 +1,343 @@
package core
import (
"bufio"
"io"
"os"
"sort"
"sync"
"time"
)
var (
// FileReadBufSize is the file read buffer size
fileReadBufSize int
// MonitorSleepTime is the duration the goroutine should periodically sleep before running file cache freshness checks
monitorSleepTime time.Duration
// FileSizeMax is the maximum file size that is alloewd to be cached
fileSizeMax int64
// FileSystem is the global FileSystem object
FileSystem *FileSystemObject
// userDir is the set subdir name to be looked for under user's home folders
userDir string
)
// FileSystemObject holds onto an LRUCacheMap and manages access to it, handless freshness checking and multi-threading
type FileSystemObject struct {
cache *lruCacheMap
sync.RWMutex
}
// NewFileSystemObject returns a new FileSystemObject
func newFileSystemObject(size int) *FileSystemObject {
return &FileSystemObject{
newLRUCacheMap(size),
sync.RWMutex{},
}
}
// StartMonitor starts the FileSystemObject freshness check monitor in its own goroutine
func (fs *FileSystemObject) StartMonitor() {
for {
// Sleep to not take up all the precious CPU time :)
time.Sleep(monitorSleepTime)
// Check file cache freshness
fs.checkCacheFreshness()
}
}
// checkCacheFreshness iterates through FileSystemObject's cache and check for freshness
func (fs *FileSystemObject) checkCacheFreshness() {
// Before anything get cache lock
fs.Lock()
fs.cache.Iterate(func(path string, f *file) {
// If this is a generated file we skip
if isGeneratedType(f) {
return
}
// Check file still exists on disk
stat, err := os.Stat(path)
if err != nil {
SystemLog.Error("Failed to stat file in cache: %s\n", path)
fs.cache.Remove(path)
return
}
// Get last mod time and check freshness
lastMod := stat.ModTime().UnixNano()
if f.IsFresh() && f.LastRefresh() < lastMod {
f.SetUnfresh()
}
})
// Done! Unlock (:
fs.Unlock()
}
// OpenFile opens a file for reading (read-only, world-readable)
func (fs *FileSystemObject) OpenFile(p *Path) (*os.File, Error) {
fd, err := os.OpenFile(p.Absolute(), os.O_RDONLY, 0444)
if err != nil {
return nil, WrapError(FileOpenErr, err)
}
return fd, nil
}
// StatFile performs a file stat on a file at path
func (fs *FileSystemObject) StatFile(p *Path) (os.FileInfo, Error) {
stat, err := os.Stat(p.Absolute())
if err != nil {
return nil, WrapError(FileStatErr, err)
}
return stat, nil
}
// ReadFile reads a supplied file descriptor into a return byte slice, or error
func (fs *FileSystemObject) ReadFile(fd *os.File) ([]byte, Error) {
// Return slice
ret := make([]byte, 0)
// Read buffer
buf := make([]byte, fileReadBufSize)
// Read through file until null bytes / error
for {
count, err := fd.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return nil, WrapError(FileReadErr, err)
}
ret = append(ret, buf[:count]...)
if count < fileReadBufSize {
break
}
}
return ret, nil
}
// ScanFile scans a supplied file at file descriptor, using iterator function
func (fs *FileSystemObject) ScanFile(fd *os.File, iterator func(string) bool) Error {
// Buffered reader
rdr := bufio.NewReaderSize(fd, fileReadBufSize)
// Iterate through file!
for {
// Line buffer
b := make([]byte, 0)
// Read until line-end, or file end!
for {
// Read a line
line, isPrefix, err := rdr.ReadLine()
if err != nil {
if err == io.EOF {
break
}
return WrapError(FileReadErr, err)
}
// Append to line buffer
b = append(b, line...)
// If not isPrefix, we can break-out
if !isPrefix {
break
}
}
// Run scan iterator on this line, break-out if requested
if !iterator(string(b)) {
break
}
}
return nil
}
// ScanDirectory reads the contents of a directory and performs the iterator function on each os.FileInfo entry returned
func (fs *FileSystemObject) ScanDirectory(fd *os.File, p *Path, iterator func(os.FileInfo, *Path)) Error {
dirList, err := fd.Readdir(-1)
if err != nil {
return WrapError(DirectoryReadErr, err)
}
// Sort by name
sort.Sort(byName(dirList))
// Walk through the directory list using supplied iterator function
for _, info := range dirList {
// Make new Path object
fp := p.JoinPath(info.Name())
// Skip restricted files
if IsRestrictedPath(fp) || WithinCGIDir(fp) {
continue
}
// Perform iterator
iterator(info, p.JoinPath(info.Name()))
}
return nil
}
// AddGeneratedFile adds a generated file content byte slice to the file cache, with supplied path as the key
func (fs *FileSystemObject) AddGeneratedFile(p *Path, b []byte) {
// Get write lock, defer unlock
fs.Lock()
defer fs.Unlock()
// Create new generatedFileContents
contents := &generatedFileContents{b}
// Wrap contents in File
file := newFile(contents)
// Add to cache!
fs.cache.Put(p.Absolute(), file)
}
// HandleClient handles a Client, attempting to serve their request from the filesystem whether a regular file, gophermap, dir listing or CGI script
func (fs *FileSystemObject) HandleClient(client *Client, request *Request, newFileContents func(*Path) FileContents, handleDirectory func(*FileSystemObject, *Client, *os.File, *Path) Error) Error {
// If restricted, return error
if IsRestrictedPath(request.Path()) {
return NewError(RestrictedPathErr)
}
// Try remap request, log if so
ok := RemapRequest(request)
if ok {
client.LogInfo(requestRemappedStr, request.Path().Selector(), request.Params())
}
// First check for file on disk
fd, err := fs.OpenFile(request.Path())
if err != nil {
// Get read-lock, defer unlock
fs.RLock()
defer fs.RUnlock()
// Don't throw in the towel yet! Check for generated file in cache
file, ok := fs.cache.Get(request.Path().Absolute())
if !ok {
return err
}
// We got a generated file! Close and send as-is
return file.WriteToClient(client, request.Path())
}
defer fd.Close()
// Get stat
stat, goErr := fd.Stat()
if goErr != nil {
// Unlock, return error
fs.RUnlock()
return WrapError(FileStatErr, goErr)
}
switch {
// Directory
case stat.Mode()&os.ModeDir != 0:
// Don't support CGI script dir enumeration
if WithinCGIDir(request.Path()) {
return NewError(RestrictedPathErr)
}
// Else enumerate dir
return handleDirectory(fs, client, fd, request.Path())
// Regular file
case stat.Mode()&os.ModeType == 0:
// Execute script if within CGI dir
if WithinCGIDir(request.Path()) {
return ExecuteCGIScript(client, request)
}
// Else just fetch
return fs.FetchFile(client, fd, stat, request.Path(), newFileContents)
// Unsupported type
default:
return NewError(FileTypeErr)
}
}
// FetchFile attempts to fetch a file from the cache, using the supplied file stat, Path and serving client. Returns Error status
func (fs *FileSystemObject) FetchFile(client *Client, fd *os.File, stat os.FileInfo, p *Path, newFileContents func(*Path) FileContents) Error {
// If file too big, write direct to client
if stat.Size() > fileSizeMax {
return client.Conn().WriteFrom(fd)
}
// Get cache read lock, defer unlock
fs.RLock()
defer fs.RUnlock()
// Now check for file in cache
f, ok := fs.cache.Get(p.Absolute())
if !ok {
// Create new file contents with supplied function
contents := newFileContents(p)
// Wrap contents in file
f = newFile(contents)
// Cache the file contents
err := f.CacheContents(fd, p)
if err != nil {
// Unlock, return error
return err
}
// Get cache write lock
fs.RUnlock()
fs.Lock()
// Put file in cache
fs.cache.Put(p.Absolute(), f)
// Switch back to cache read lock, get file read lock
fs.Unlock()
fs.RLock()
f.RLock()
} else {
// Get file read lock
f.RLock()
// Check for file freshness
if !f.IsFresh() {
// Switch to file write lock
f.RUnlock()
f.Lock()
// Refresh file contents
err := f.CacheContents(fd, p)
if err != nil {
// Unlock file, return error
f.Unlock()
return err
}
// Done! Switch back to read lock
f.Unlock()
f.RLock()
}
}
// Defer file unlock, write to client
defer f.RUnlock()
return f.WriteToClient(client, p)
}

@ -0,0 +1,18 @@
package core
var (
// Root stores the server's root directory
Root string
// BindAddr stores the server's bound IP
BindAddr string
// Hostname stores the host's outward hostname
Hostname string
// Port stores the internal port the host is binded to
Port string
// FwdPort stores the host's outward port number
FwdPort string
)

@ -0,0 +1,37 @@
package core
import "net"
// serverListener holds the global Listener object
var serverListener *listener
// listener wraps a net.TCPListener to return our own clients on each Accept()
type listener struct {
l *net.TCPListener
}
// NewListener returns a new Listener or Error
func newListener(ip, port string) (*listener, Error) {
// Try resolve provided ip and port details
laddr, err := net.ResolveTCPAddr("tcp", ip+":"+port)
if err != nil {
return nil, WrapError(ListenerResolveErr, err)
}
// Create listener!
l, err := net.ListenTCP("tcp", laddr)
if err != nil {
return nil, WrapError(ListenerBeginErr, err)
}
return &listener{l}, nil
}
// Accept accepts a new connection and returns a client, or error
func (l *listener) Accept() (*Client, Error) {
conn, err := l.l.AcceptTCP()
if err != nil {
return nil, WrapError(ListenerAcceptErr, err)
}
return NewClient(conn), nil
}

@ -0,0 +1,92 @@
package core
import (
"log"
"os"
)
var (
// AccessLog holds a global access LogObject
AccessLog loggerInterface
// SystemLog holds a global system LogObject
SystemLog loggerInterface
)
func setupLogger(output string) loggerInterface {
switch output {
case "stdout":
return &stdLogger{}
case "null":
return &nullLogger{}
default:
fd, err := os.OpenFile(output, os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
log.Fatalf(logOutputErrStr, output, err.Error())
}
return &logger{log.New(fd, "", log.LstdFlags)}
}
}
// LoggerInterface specifies an interface that can log different message levels
type loggerInterface interface {
Info(string, ...interface{})
Error(string, ...interface{})
Fatal(string, ...interface{})
}
// StdLogger implements LoggerInterface to log to output using regular log
type stdLogger struct{}
// Info logs to log.Logger with info level prefix
func (l *stdLogger) Info(fmt string, args ...interface{}) {
log.Printf(":: I :: "+fmt, args...)
}
// Error logs to log.Logger with error level prefix
func (l *stdLogger) Error(fmt string, args ...interface{}) {
log.Printf(":: E :: "+fmt, args...)
}
// Fatal logs to standard log with fatal prefix and terminates program
func (l *stdLogger) Fatal(fmt string, args ...interface{}) {
log.Fatalf(":: F :: "+fmt, args...)
}
// logger implements LoggerInterface to log to output using underlying log.Logger
type logger struct {
logger *log.Logger
}
// Info logs to log.Logger with info level prefix
func (l *logger) Info(fmt string, args ...interface{}) {
l.logger.Printf("I :: "+fmt, args...)
}
// Error logs to log.Logger with error level prefix
func (l *logger) Error(fmt string, args ...interface{}) {
l.logger.Printf("E :: "+fmt, args...)
}
// Fatal logs to log.Logger with fatal prefix and terminates program
func (l *logger) Fatal(fmt string, args ...interface{}) {
l.logger.Fatalf("F :: "+fmt, args...)
}
// nullLogger implements LoggerInterface to do absolutely fuck-all
type nullLogger struct{}
// Info does nothing
func (l *nullLogger) Info(fmt string, args ...interface{}) {
// do nothing
}
// Error does nothing
func (l *nullLogger) Error(fmt string, args ...interface{}) {
// do nothing
}
// Fatal simply terminates the program
func (l *nullLogger) Fatal(fmt string, args ...interface{}) {
os.Exit(1)
}

@ -0,0 +1,118 @@
package core
import (
"path"
"strings"
)
// Path safely holds a file path
type Path struct {
root string // root dir
rel string // relative path
sel string // selector path
}
// NewPath returns a new Path structure based on supplied root and relative path
func NewPath(root, rel string) *Path {
return &Path{root, rel, formatSelector(rel)}
}
// newSanitizedPath returns a new sanitized Path structure based on root and relative path
func newSanitizedPath(root, rel string) *Path {
return NewPath(root, sanitizeRawPath(root, rel))
}
// Remap remaps a Path to a new relative path, keeping previous selector
func (p *Path) Remap(newRel string) {
p.rel = sanitizeRawPath(p.root, newRel)
}
// Root returns file's root directory
func (p *Path) Root() string {
return p.root
}
// Relative returns the relative path
func (p *Path) Relative() string {
return p.rel
}
// Absolute returns the absolute path
func (p *Path) Absolute() string {
return path.Join(p.root, p.rel)
}
// Selector returns the formatted selector path
func (p *Path) Selector() string {
return p.sel
}
// RelativeDir returns the residing dir of the relative path
func (p *Path) RelativeDir() string {
return path.Dir(p.rel)
}
// SelectorDir returns the residing dir of the selector path
func (p *Path) SelectorDir() string {
return path.Dir(p.sel)
}
// Dir returns a Path object at the residing dir of the calling object (keeping separate selector intact)
func (p *Path) Dir() *Path {
return &Path{p.root, p.RelativeDir(), p.SelectorDir()}
}
// JoinRelative returns a string appended to the current relative path
func (p *Path) JoinRelative(newRel string) string {
return path.Join(p.rel, newRel)
}
// JoinPath appends the supplied string to the Path's relative and selector paths
func (p *Path) JoinPath(toJoin string) *Path {
return &Path{p.root, path.Join(p.rel, toJoin), path.Join(p.sel, toJoin)}
}
// formatSelector formats a relative path to a valid selector path
func formatSelector(rel string) string {
switch len(rel) {
case 0:
return "/"
case 1:
if rel[0] == '.' {
return "/"
}
return "/" + rel
default:
if rel[0] == '/' {
return rel
}
return "/" + rel
}
}
// sanitizeRawPath takes a root and relative path, and returns a sanitized relative path
func sanitizeRawPath(root, rel string) string {
// Start by cleaning
rel = path.Clean(rel)
if path.IsAbs(rel) {
// Absolute path, try trimming root and leading '/'
rel = strings.TrimPrefix(strings.TrimPrefix(rel, root), "/")
} else {
// Relative path, if back dir traversal give them server root
if strings.HasPrefix(rel, "..") {
rel = ""
}
}
return rel
}
// sanitizerUserRoot takes a generated user root directory and sanitizes it, returning a bool as to whether it's safe
func sanitizeUserRoot(root string) (string, bool) {
root = path.Clean(root)
if !strings.HasPrefix(root, "/home/") && strings.HasSuffix(root, "/"+userDir) {
return "", false
}
return root, true
}

@ -0,0 +1,159 @@
package core
import (
"path"
"regexp"
"strings"
)
var (
// cgiDir is a precompiled regex statement to check if a string matches the server's CGI directory
cgiDirRegex *regexp.Regexp
// WithinCGIDir returns whether a path is within the server's specified CGI scripts directory
WithinCGIDir func(*Path) bool
// RestrictedPaths is the global slice of restricted paths
restrictedPaths []*regexp.Regexp
// IsRestrictedPath is the global function to check against restricted paths
IsRestrictedPath func(*Path) bool
// requestRemaps is the global slice of remapped paths
requestRemaps []*RequestRemap
// RemapRequest is the global function to remap a request
RemapRequest func(*Request) bool
)
// PathMapSeparatorStr specifies the separator string to recognise in path mappings
const requestRemapSeparatorStr = " -> "
// RequestRemap is a structure to hold a remap regex to check against, and a template to apply this transformation onto
type RequestRemap struct {
Regex *regexp.Regexp
Template string
}
// compileCGIRegex takes a supplied string and returns compiled regular expression
func compileCGIRegex(cgiDir string) *regexp.Regexp {
if path.IsAbs(cgiDir) {
if !strings.HasPrefix(cgiDir, Root) {
SystemLog.Fatal(cgiDirOutsideRootStr)
}
} else {
cgiDir = path.Join(Root, cgiDir)
}
SystemLog.Info(cgiDirStr, cgiDir)
return regexp.MustCompile("(?m)" + cgiDir + "(|/.*)$")
}
// compileRestrictedPathsRegex turns a string of restricted paths into a slice of compiled regular expressions
func compileRestrictedPathsRegex(restrictions string) []*regexp.Regexp {
regexes := make([]*regexp.Regexp, 0)
// Split restrictions string by new lines
for _, expr := range strings.Split(restrictions, "\n") {
// Skip empty expressions
if len(expr) == 0 {
continue
}
// Compile the regular expression
regex, err := regexp.Compile("(?m)" + expr + "$")
if err != nil {
SystemLog.Fatal(pathRestrictRegexCompileFailStr, expr)
}
// Append compiled regex and log
regexes = append(regexes, regex)
SystemLog.Info(pathRestrictRegexCompiledStr, expr)
}
return regexes
}
// compil RequestRemapRegex turns a string of remapped paths into a slice of compiled RequestRemap structures
func compileRequestRemapRegex(remaps string) []*RequestRemap {
requestRemaps := make([]*RequestRemap, 0)
// Split remaps string by new lines
for _, expr := range strings.Split(remaps, "\n") {
// Skip empty expressions
if len(expr) == 0 {
continue
}
// Split into alias and remap
split := strings.Split(expr, requestRemapSeparatorStr)
if len(split) != 2 {
SystemLog.Fatal(requestRemapRegexInvalidStr, expr)
}
// Compile the regular expression
regex, err := regexp.Compile("(?m)" + strings.TrimPrefix(split[0], "/") + "$")
if err != nil {
SystemLog.Fatal(requestRemapRegexCompileFailStr, expr)
}
// Append RequestRemap and log
requestRemaps = append(requestRemaps, &RequestRemap{regex, strings.TrimPrefix(split[1], "/")})
SystemLog.Info(requestRemapRegexCompiledStr, expr)
}
return requestRemaps
}
// withinCGIDirEnabled returns whether a Path's absolute value matches within the CGI dir
func withinCGIDirEnabled(p *Path) bool {
return cgiDirRegex.MatchString(p.Absolute())
}
// withinCGIDirDisabled always returns false, CGI is disabled
func withinCGIDirDisabled(p *Path) bool {
return false
}
// isRestrictedPathEnabled returns whether a Path's relative value is restricted
func isRestrictedPathEnabled(p *Path) bool {
for _, regex := range restrictedPaths {
if regex.MatchString(p.Relative()) {
return true
}
}
return false
}
// isRestrictedPathDisabled always returns false, there are no restricted paths
func isRestrictedPathDisabled(p *Path) bool {
return false
}
// remapRequestEnabled tries to remap a request, returning bool as to success
func remapRequestEnabled(request *Request) bool {
for _, remap := range requestRemaps {
// No match, gotta keep looking
if !remap.Regex.MatchString(request.Path().Selector()) {
continue
}
// Create new request from template and submatches
raw := make([]byte, 0)
for _, submatches := range remap.Regex.FindAllStringSubmatchIndex(request.Path().Selector(), -1) {
raw = remap.Regex.ExpandString(raw, remap.Template, request.Path().Selector(), submatches)
}
// Split to new path and paramters again
path, params := splitBy(string(raw), "?")
// Remap request, log, return
request.Remap(path, params)
return true
}
return false
}
// remapRequestDisabled always returns false, there are no remapped requests
func remapRequestDisabled(request *Request) bool {
return false
}

@ -0,0 +1,25 @@
package core
// Request is a data structure for storing a filesystem path, and params, parsed from a client's request
type Request struct {
p *Path
params string
}
// Path returns the requests associate Path object
func (r *Request) Path() *Path {
return r.p
}
// Params returns the request's parameters string
func (r *Request) Params() string {
return r.params
}
// Remap modifies a request to use new relative path, and accommodate supplied extra parameters
func (r *Request) Remap(rel, params string) {
if len(r.params) > 0 {
r.params = params + "&" + r.params
}
r.p.Remap(rel)
}

@ -0,0 +1,208 @@
package core
import (
"flag"
"fmt"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
"time"
)
const (
// Version holds the current version string
Version = "v0.3-alpha"
)
var (
// SigChannel is the global OS signal channel
sigChannel chan os.Signal
)
// ParseFlagsAndSetup parses necessary core server flags, and sets up the core ready for Start() to be called
func ParseFlagsAndSetup(errorMessageFunc func(ErrorCode) string) {
// Setup numerous temporary flag variables, and store the rest
// directly in their final operating location. Strings are stored
// in `string_constants.go` to allow for later localization
sysLog := flag.String(sysLogFlagStr, "stdout", sysLogDescStr)
accLog := flag.String(accLogFlagStr, "stdout", accLogDescStr)
flag.StringVar(&Root, rootFlagStr, "/var/gopher", rootDescStr)
flag.StringVar(&BindAddr, bindAddrFlagStr, "", bindAddrDescStr)
flag.StringVar(&Hostname, hostnameFlagStr, "localhost", hostnameDescStr)
port := flag.Uint(portFlagStr, 70, portDescStr)
fwdPort := flag.Uint(fwdPortFlagStr, 0, fwdPortDescStr)
flag.DurationVar(&connReadDeadline, readDeadlineFlagStr, time.Duration(time.Second*3), readDeadlineDescStr)
flag.DurationVar(&connWriteDeadline, writeDeadlineFlagStr, time.Duration(time.Second*5), writeDeadlineDescStr)
cReadBuf := flag.Uint(connReadBufFlagStr, 1024, connReadBufDescStr)
cWriteBuf := flag.Uint(connWriteBufFlagStr, 1024, connWriteBufDescStr)
cReadMax := flag.Uint(connReadMaxFlagStr, 4096, connReadMaxDescStr)
fReadBuf := flag.Uint(fileReadBufFlagStr, 1024, fileReadBufDescStr)
flag.DurationVar(&monitorSleepTime, monitorSleepTimeFlagStr, time.Duration(time.Second*1), monitorSleepTimeDescStr)
cacheMax := flag.Float64(cacheFileMaxFlagStr, 1.0, cacheFileMaxDescStr)
cacheSize := flag.Uint(cacheSizeFlagStr, 100, cacheSizeDescStr)
restrictedPathsList := flag.String(restrictPathsFlagStr, "", restrictPathsDescStr)
remapRequestsList := flag.String(remapRequestsFlagStr, "", remapRequestsDescStr)
cgiDir := flag.String(cgiDirFlagStr, "", cgiDirDescStr)
flag.DurationVar(&maxCGIRunTime, maxCGITimeFlagStr, time.Duration(time.Second*3), maxCGITimeDescStr)
safePath := flag.String(safePathFlagStr, "/bin:/usr/bin", safePathDescStr)
httpCompatCGI := flag.Bool(httpCompatCGIFlagStr, false, httpCompatCGIDescStr)
httpPrefixBuf := flag.Uint(httpPrefixBufFlagStr, 1024, httpPrefixBufDescStr)
flag.StringVar(&userDir, userDirFlagStr, "", userDirDescStr)
printVersion := flag.Bool(versionFlagStr, false, versionDescStr)
// Parse flags! (including any set by outer calling function)
flag.Parse()
// If version print requested, do so!
if *printVersion {
fmt.Println("Gophor " + Version)
os.Exit(0)
}
// Setup loggers
SystemLog = setupLogger(*sysLog)
if sysLog == accLog {
AccessLog = SystemLog
} else {
AccessLog = setupLogger(*accLog)
}
// Check valid values for BindAddr and Hostname
if Hostname == "" {
if BindAddr == "" {
SystemLog.Fatal(hostnameBindAddrEmptyStr)
}
Hostname = BindAddr
}
// Change to server directory
if osErr := os.Chdir(Root); osErr != nil {
SystemLog.Fatal(chDirErrStr, osErr)
}
SystemLog.Info(chDirStr, Root)
// Set port info
if *fwdPort == 0 {
fwdPort = port
}
Port = strconv.Itoa(int(*port))
FwdPort = strconv.Itoa(int(*fwdPort))
// Setup listener
var err Error
serverListener, err = newListener(BindAddr, Port)
if err != nil {
SystemLog.Fatal(listenerBeginFailStr, BindAddr, Port, err.Error())
}
// Host buffer sizes
connReadBufSize = int(*cReadBuf)
connWriteBufSize = int(*cWriteBuf)
connReadMax = int(*cReadMax)
fileReadBufSize = int(*fReadBuf)
// FileSystemObject (and related) setup
fileSizeMax = int64(1048576.0 * *cacheMax) // gets megabytes value in bytes
FileSystem = newFileSystemObject(int(*cacheSize))
// If no restricted files provided, set to the disabled function. Else, compile and enable
if *restrictedPathsList == "" {
SystemLog.Info(pathRestrictionsDisabledStr)
IsRestrictedPath = isRestrictedPathDisabled
} else {
SystemLog.Info(pathRestrictionsEnabledStr)
restrictedPaths = compileRestrictedPathsRegex(*restrictedPathsList)
IsRestrictedPath = isRestrictedPathEnabled
}
// If no remapped files provided, set to the disabled function. Else, compile and enable
if *remapRequestsList == "" {
SystemLog.Info(requestRemapDisabledStr)
RemapRequest = remapRequestDisabled
} else {
SystemLog.Info(requestRemapEnabledStr)
requestRemaps = compileRequestRemapRegex(*remapRequestsList)
RemapRequest = remapRequestEnabled
}
// If no CGI dir supplied, set to disabled function. Else, compile and enable
if *cgiDir == "" {
SystemLog.Info(cgiSupportDisabledStr)
WithinCGIDir = withinCGIDirDisabled
} else {
SystemLog.Info(cgiSupportEnabledStr)
cgiDirRegex = compileCGIRegex(*cgiDir)
cgiEnv = setupInitialCGIEnv(*safePath)
WithinCGIDir = withinCGIDirEnabled
// Enable HTTP compatible CGI scripts, or not
if *httpCompatCGI {
SystemLog.Info(cgiHTTPCompatEnabledStr, httpPrefixBuf)
ExecuteCGIScript = executeCGIScriptStripHTTP
httpPrefixBufSize = int(*httpPrefixBuf)
} else {
ExecuteCGIScript = executeCGIScriptNoHTTP
}
}
// If no user dir supplied, set to disabled function. Else, set user dir and enable
if userDir == "" {
SystemLog.Info(userDirDisabledStr)
getRequestPath = getRequestPathUserDirDisabled
} else {
SystemLog.Info(userDirEnabledStr)
getRequestPath = getRequestPathUserDirEnabled
// Clean the user dir to be safe
userDir = path.Clean(userDir)
if strings.HasPrefix(userDir, "..") {
SystemLog.Fatal(userDirBackTraverseErrStr, userDir)
} else {
SystemLog.Info(userDirStr, userDir)
}
}
// Set ErrorCode->string function
getExtendedErrorMessage = errorMessageFunc
// Setup signal channel
sigChannel = make(chan os.Signal)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
}
// Start begins operation of the server
func Start(serve func(*Client)) {
// Start the FileSystemObject cache freshness monitor
SystemLog.Info(cacheMonitorStartStr, monitorSleepTime)
go FileSystem.StartMonitor()
// Start the listener
SystemLog.Info(listeningOnStr, BindAddr, Port, Hostname, FwdPort)
go func() {
for {
client, err := serverListener.Accept()
if err != nil {
SystemLog.Error(err.Error())
}
// Serve client then close in separate goroutine
go func() {
serve(client)
client.Conn().Close()
}()
}
}()
// Listen for OS signals and terminate if necessary
listenForOSSignals()
}
// ListenForOSSignals listens for OS signals and terminates the program if necessary
func listenForOSSignals() {
sig := <-sigChannel
SystemLog.Info(signalReceivedStr, sig)
os.Exit(0)
}

@ -0,0 +1,151 @@
package core
// Core flag string constants
const (
sysLogFlagStr = "sys-log"
sysLogDescStr = "System log output location ['stdout', 'null', $filename]"
accLogFlagStr = "acc-log"
accLogDescStr = "Access log output location ['stdout', 'null', $filename]"
rootFlagStr = "root"
rootDescStr = "Server root directory"
bindAddrFlagStr = "bind-addr"
bindAddrDescStr = "IP address to bind to"
hostnameFlagStr = "hostname"
hostnameDescStr = "Server hostname (FQDN)"
portFlagStr = "port"
portDescStr = "Port to listen on"
fwdPortFlagStr = "fwd-port"
fwdPortDescStr = "Outward-facing port"
readDeadlineFlagStr = "read-deadline"
readDeadlineDescStr = "Connection read deadline (timeout)"
writeDeadlineFlagStr = "write-deadline"
writeDeadlineDescStr = "Connection write deadline (timeout)"
connReadBufFlagStr = "conn-read-buf"
connReadBufDescStr = "Connection read buffer size (bytes)"
connWriteBufFlagStr = "conn-write-buf"
connWriteBufDescStr = "Connection write buffer size (bytes)"
connReadMaxFlagStr = "conn-read-max"
connReadMaxDescStr = "Connection read max (bytes)"
fileReadBufFlagStr = "file-read-buf"
fileReadBufDescStr = "File read buffer size (bytes)"
monitorSleepTimeFlagStr = "cache-monitor-freq"
monitorSleepTimeDescStr = "File cache freshness monitor frequency"
cacheFileMaxFlagStr = "cache-file-max"
cacheFileMaxDescStr = "Max cached file size (megabytes)"
cacheSizeFlagStr = "cache-size"
cacheSizeDescStr = "File cache size"
restrictPathsFlagStr = "restrict-paths"
restrictPathsDescStr = "Restrict paths as new-line separated list of regex statements (see documenation)"
remapRequestsFlagStr = "remap-requests"
remapRequestsDescStr = "Remap requests as new-line separated list of remap statements (see documenation)"
cgiDirFlagStr = "cgi-dir"
cgiDirDescStr = "CGI scripts directory (empty to disable)"
maxCGITimeFlagStr = "max-cgi-time"
maxCGITimeDescStr = "Max CGI script execution time"
safePathFlagStr = "safe-path"
safePathDescStr = "CGI environment safe PATH variable"
httpCompatCGIFlagStr = "http-compat-cgi"
httpCompatCGIDescStr = "Enable HTTP compatibility for CGI scripts by stripping headers"
httpPrefixBufFlagStr = "http-prefix-buf"
httpPrefixBufDescStr = "Buffer size used for stripping HTTP headers"
userDirFlagStr = "user-dir"
userDirDescStr = "User's personal server directory"
versionFlagStr = "version"
versionDescStr = "Print version string"
)
// Log string constants
const (
hostnameBindAddrEmptyStr = "At least one of hostname or bind-addr must be non-empty!"
chDirStr = "Entered server dir: %s"
chDirErrStr = "Error entering server directory: %s"
listenerBeginFailStr = "Failed to start listener on %s:%s (%s)"
listeningOnStr = "Listening on: %s:%s (%s:%s)"
cacheMonitorStartStr = "Starting cache monitor with freq: %s"
pathRestrictionsEnabledStr = "Path restrictions enabled"
pathRestrictionsDisabledStr = "Path restrictions disabled"
pathRestrictRegexCompileFailStr = "Failed compiling restricted path regex: %s"
pathRestrictRegexCompiledStr = "Compiled restricted path regex: %s"
requestRemapEnabledStr = "Request remapping enabled"
requestRemapDisabledStr = "Request remapping disabled"
requestRemapRegexInvalidStr = "Invalid request remap regex: %s"
requestRemapRegexCompileFailStr = "Failed compiling request remap regex: %s"
requestRemapRegexCompiledStr = "Compiled path remap regex: %s"
requestRemappedStr = "Remapped request: %s %s"
cgiSupportEnabledStr = "CGI script support enabled"
cgiSupportDisabledStr = "CGI script support disabled"
cgiDirOutsideRootStr = "CGI directory must not be outside server root!"
cgiDirStr = "CGI directory: %s"
cgiHTTPCompatEnabledStr = "CGI HTTP compatibility enabled, prefix buffer: %d"
cgiExecuteErrStr = "Exit executing: %s [%d]"
userDirEnabledStr = "User directory support enabled"
userDirDisabledStr = "User directory support disabled"
userDirBackTraverseErrStr = "User directory with back-traversal not supported: %s"
userDirStr = "User directory: %s"
signalReceivedStr = "Signal received: %v. Shutting down..."
logOutputErrStr = "Error opening log output %s: %s"
pgidNotFoundErrStr = "Process unfinished, PGID not found!"
pgidStopErrStr = "Error stopping process group %d: %s"
connWriteErrStr = "Conn write error"
connReadErrStr = "Conn read error"
connCloseErrStr = "Conn close error"
listenerResolveErrStr = "Listener resolve error"
listenerBeginErrStr = "Listener begin error"
listenerAcceptErrStr = "Listener accept error"
invalidIPErrStr = "Invalid IP"
invalidPortErrStr = "Invalid port"
fileOpenErrStr = "File open error"
fileStatErrStr = "File stat error"
fileReadErrStr = "File read error"
fileTypeErrStr = "Unsupported file type"
directoryReadErrStr = "Directory read error"
restrictedPathErrStr = "Restricted path"
invalidRequestErrStr = "Invalid request"
cgiStartErrStr = "CGI start error"
cgiExitCodeErrStr = "CGI non-zero exit code"
cgiStatus400ErrStr = "CGI status: 400"
cgiStatus401ErrStr = "CGI status: 401"
cgiStatus403ErrStr = "CGI status: 403"
cgiStatus404ErrStr = "CGI status: 404"
cgiStatus408ErrStr = "CGI status: 408"
cgiStatus410ErrStr = "CGI status: 410"
cgiStatus500ErrStr = "CGI status: 500"
cgiStatus501ErrStr = "CGI status: 501"
cgiStatus503ErrStr = "CGI status: 503"
cgiStatusUnknownErrStr = "CGI status: unknown"
)

@ -0,0 +1,75 @@
package core
import (
"net/url"
"path"
"strings"
)
var (
// getRequestPaths points to either of the getRequestPath____ functions
getRequestPath func(string) *Path
)
// ParseURLEncodedRequest takes a received string and safely parses a request from this
func ParseURLEncodedRequest(received string) (*Request, Error) {
// Check for ASCII control bytes
for i := 0; i < len(received); i++ {
if received[i] < ' ' || received[i] == 0x7f {
return nil, NewError(InvalidRequestErr)
}
}
// Split into 2 substrings by '?'. URL path and query
rawPath, params := splitBy(received, "?")
// Unescape path
rawPath, err := url.PathUnescape(rawPath)
if err != nil {
return nil, WrapError(InvalidRequestErr, err)
}
// Return new request
return &Request{getRequestPath(rawPath), params}, nil
}
// ParseInternalRequest parses an internal request string based on the current directory
func ParseInternalRequest(p *Path, line string) *Request {
rawPath, params := splitBy(line, "?")
if path.IsAbs(rawPath) {
return &Request{getRequestPath(rawPath), params}
}
return &Request{newSanitizedPath(p.Root(), rawPath), params}
}
// getRequestPathUserDirEnabled creates a Path object from raw path, converting ~USER to user subdirectory roots, else at server root
func getRequestPathUserDirEnabled(rawPath string) *Path {
if userPath := strings.TrimPrefix(rawPath, "/"); strings.HasPrefix(userPath, "~") {
// We found a user path! Split into the user part, and remaining path
user, remaining := splitBy(userPath, "/")
// Empty user, we been duped! Return server root
if len(user) <= 1 {
return &Path{Root, "", "/"}
}
// Get sanitized user root, else return server root
root, ok := sanitizeUserRoot(path.Join("/home", user[1:], userDir))
if !ok {
return &Path{Root, "", "/"}
}
// Build new Path
rel := sanitizeRawPath(root, remaining)
sel := "/~" + user[1:] + formatSelector(rel)
return &Path{root, rel, sel}
}
// Return regular server root + rawPath
return newSanitizedPath(Root, rawPath)
}
// getRequestPathUserDirDisabled creates a Path object from raw path, always at server root
func getRequestPathUserDirDisabled(rawPath string) *Path {
return newSanitizedPath(Root, rawPath)
}

@ -0,0 +1,22 @@
package core
import (
"os"
"strings"
)
// byName and its associated functions provide a quick method of sorting FileInfos by name
type byName []os.FileInfo
func (s byName) Len() int { return len(s) }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// SplitBy takes an input string and a delimiter, returning the resulting two strings from the split (ALWAYS 2)
func splitBy(input, delim string) (string, string) {
split := strings.SplitN(input, delim, 2)
if len(split) == 2 {
return split[0], split[1]
}
return split[0], ""
}

@ -1,251 +0,0 @@
package main
import (
"fmt"
)
/* Simple error code type defs */
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
/* Sockets */
SocketWriteErr ErrorCode = iota
SocketWriteRawErr ErrorCode = iota
/* Parsing */
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
ErrorResponse400 ErrorResponseCode = iota
ErrorResponse401 ErrorResponseCode = iota
ErrorResponse403 ErrorResponseCode = iota
ErrorResponse404 ErrorResponseCode = iota
ErrorResponse408 ErrorResponseCode = iota
ErrorResponse410 ErrorResponseCode = iota
ErrorResponse500 ErrorResponseCode = iota
ErrorResponse501 ErrorResponseCode = iota
ErrorResponse503 ErrorResponseCode = iota
NoResponse ErrorResponseCode = iota
)
/* Simple GophorError data structure to wrap another error */
type GophorError struct {
Code ErrorCode
Err error
}
/* Convert error code to string */
func (e *GophorError) Error() string {
var str string
switch e.Code {
case PathEnumerationErr:
str = "path enumeration fail"
case IllegalPathErr:
str = "illegal path requested"
case FileStatErr:
str = "file stat fail"
case FileOpenErr:
str = "file open fail"
case FileReadErr:
str = "file read fail"
case FileTypeErr:
str = "invalid file type"
case DirListErr:
str = "directory read fail"
case SocketWriteErr:
str = "socket write error"
case SocketWriteRawErr:
str = "socket write readFrom error"
case InvalidRequestErr:
str = "invalid request data"
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"
}
if e.Err != nil {
return fmt.Sprintf("%s (%s)", str, e.Err.Error())
} else {
return fmt.Sprintf("%s", str)
}
}
/* Convert a gophor error code to appropriate error response code */
func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
switch code {
case PathEnumerationErr:
return ErrorResponse400
case IllegalPathErr:
return ErrorResponse403
case FileStatErr:
return ErrorResponse404
case FileOpenErr:
return ErrorResponse404
case FileReadErr:
return ErrorResponse404
case FileTypeErr:
/* If wrong file type, just assume file not there */
return ErrorResponse404
case DirListErr:
return ErrorResponse404
/* These are errors _while_ sending, no point trying to send error */
case SocketWriteErr:
return NoResponse
case SocketWriteRawErr:
return NoResponse
case InvalidRequestErr:
return ErrorResponse400
case InvalidGophermapErr:
return ErrorResponse500
case CommandStartErr:
return ErrorResponse500
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:
return ErrorResponse503
}
}
/* Generates gopher protocol compatible error response from our code */
func generateGopherErrorResponseFromCode(code ErrorCode) []byte {
responseCode := gophorErrorToResponseCode(code)
if responseCode == NoResponse {
return nil
}
return generateGopherErrorResponse(responseCode)
}
/* Generates gopher protocol compatible error response for response code */
func generateGopherErrorResponse(code ErrorResponseCode) []byte {
return buildErrorLine(code.String())
}
/* Error response code to string */
func (e ErrorResponseCode) String() string {
switch e {
case ErrorResponse200:
/* 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:
return "401 Unauthorised"
case ErrorResponse403:
return "403 Forbidden"
case ErrorResponse404:
return "404 Not Found"
case ErrorResponse408:
return "408 Request Time-out"
case ErrorResponse410:
return "410 Gone"
case ErrorResponse500:
return "500 Internal Server Error"
case ErrorResponse501:
return "501 Not Implemented"
case ErrorResponse503:
return "503 Service Unavailable"
default:
/* Should not have reached here */
Config.SysLog.Fatal("", "Unhandled ErrorResponseCode type\n")
return ""
}
}

@ -1,160 +0,0 @@
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
}

@ -1,353 +0,0 @@
package main
import (
"bytes"
"bufio"
"os"
)
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 {
/* Super simple, holds onto a slice of bytes */
Contents []byte
}
func (fc *GeneratedFileContents) Render(responder *Responder) *GophorError {
return responder.WriteData(fc.Contents)
}
func (fc *GeneratedFileContents) Load() *GophorError {
/* do nothing */
return nil
}
func (fc *GeneratedFileContents) Clear() {
/* do nothing */
}
type RegularFileContents struct {
/* Simple implemention that holds onto a RequestPath
* and slice containing cache'd content
*/
Path *RequestPath
Contents []byte
}
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.Absolute())
return gophorErr
}
func (fc *RegularFileContents) Clear() {
fc.Contents = nil
}
type GophermapContents struct {
/* Holds onto a RequestPath and slice containing individually
* renderable sections of the gophermap.
*/
Request *Request
Sections []GophermapSection
}
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 {
Config.SysLog.Error("", "Error executing gophermap contents: %s\n", gophorErr.Error())
return &GophorError{ InvalidGophermapErr, gophorErr }
}
}
/* 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.Request)
if gophorErr != nil {
return &GophorError{ InvalidGophermapErr, gophorErr }
} else {
return nil
}
}
func (gc *GophermapContents) Clear() {
gc.Sections = nil
}
type GophermapSection interface {
/* Interface for storing differring types of gophermap
* sections to render when necessary
*/
Render(*Responder) *GophorError
}
type GophermapTextSection struct {
Contents []byte
}
func (s *GophermapTextSection) Render(responder *Responder) *GophorError {
return responder.WriteData(replaceStrings(string(s.Contents), responder.Host))
}
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
}
func (g *GophermapDirectorySection) Render(responder *Responder) *GophorError {
/* Create new responder from supplied and using stored path */
return listDir(responder.CloneWithRequest(g.Request), g.Hidden)
}
type GophermapFileSection struct {
/* Holds onto a file path to be read and rendered when requested */
Request *Request
}
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 (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 := 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
/* Error setting within nested function below */
var returnErr *GophorError
/* Perform buffered scan with our supplied splitter and iterators */
gophorErr := bufferedScan(request.Path.Absolute(),
func(scanner *bufio.Scanner) bool {
line := scanner.Text()
/* Parse the line item type and handle */
lineType := parseLineType(line)
switch lineType {
case TypeInfoNotStated:
/* Append TypeInfo to the beginning of 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, &GophermapTextSection{ buildLine(TypeInfo, line[1:], "TITLE", NullHost, NullPort) })
titleAlready = true
}
case TypeComment:
/* We ignore this line */
break
case TypeHiddenFile:
/* Add to hidden files map */
hidden[request.Path.JoinRel(line[1:])] = true
case TypeSubGophermap:
/* 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
}
/* 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, &GophermapSubmapSection{ subRequest })
}
} else {
/* If stored in cgi-bin store as GophermapExecCgiSection, else GophermapFileSection */
if withinCgiBin(subRequest.Path.Relative()) {
sections = append(sections, &GophermapExecCgiSection{ subRequest })
} else {
sections = append(sections, &GophermapFileSection{ subRequest })
}
}
case TypeEnd:
/* 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:
/* 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:
/* Default is appending to sections slice as GopherMapTextSection */
sections = append(sections, &GophermapTextSection{ []byte(line+DOSLineEnd) })
}
return true
},
)
/* Check the bufferedScan didn't exit with error */
if gophorErr != nil {
return nil, gophorErr
} 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 iterator */
gophorErr := bufferedScan(path,
func(scanner *bufio.Scanner) bool {
line := scanner.Text()
if line == "" {
fileContents = append(fileContents, buildInfoLine("")...)
return true
}
/* Replace the newline characters */
line = replaceNewLines(line)
/* Iterate through line string, reflowing to new line
* until all lines < PageWidth
*/
for len(line) > 0 {
length := minWidth(len(line))
fileContents = append(fileContents, buildInfoLine(line[:length])...)
line = line[length:]
}
return true
},
)
/* Check the bufferedScan didn't exit with error */
if gophorErr != nil {
return nil, gophorErr
}
/* Check final output ends on a newline */
if !bytes.HasSuffix(fileContents, []byte(DOSLineEnd)) {
fileContents = append(fileContents, []byte(DOSLineEnd)...)
}
return fileContents, nil
}
/* Return minimum width out of PageWidth and W */
func minWidth(w int) int {
if w <= Config.PageWidth {
return w
} else {
return Config.PageWidth
}
}

@ -1,351 +0,0 @@
package main
import (
"os"
"sync"
"time"
"regexp"
)
const (
/* Help converting file size stat to supplied size in megabytes */
BytesInMegaByte = 1048576.0
/* Filename constants */
CgiBinDirStr = "cgi-bin"
GophermapFileStr = "gophermap"
)
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) IsRestricted(path string) bool {
for _, regex := range fs.Restricted {
if regex.MatchString(path) {
return true
}
}
return false
}
func (fs *FileSystem) RemapRequestPath(requestPath *RequestPath) (*RequestPath, bool) {
for _, remap := range fs.Remaps {
/* No match :( keep lookin */
if !remap.Regex.MatchString(requestPath.Relative()) {
continue
}
/* 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 this new path to the _actual_ path */
return requestPath.RemapPath(string(newPath)), true
}
return nil, false
}
func (fs *FileSystem) HandleRequest(responder *Responder) *GophorError {
/* Check if restricted file */
if fs.IsRestricted(responder.Request.Path.Relative()) {
return &GophorError{ IllegalPathErr, nil }
}
/* 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 {
/* Directory */
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 := NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(GophermapFileStr))
stat, err = os.Stat(gophermapPath.Absolute())
if err == nil {
/* 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 */
return listDirAsGophermap(responder, map[string]bool{ gophermapPath.Relative(): true, CgiBinDirStr: true })
}
/* Regular file */
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 &GophorError{ FileTypeErr, nil }
}
}
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(responder.Request.Path.Absolute())
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.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
file.Mutex.Unlock()
fs.CacheMutex.RUnlock()
return gophorErr
}
/* Updated! Swap back file write for read lock */
file.Mutex.Unlock()
file.Mutex.RLock()
}
} else {
/* 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
*/
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 &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 */
var contents FileContents
if isGophermap(responder.Request.Path.Relative()) {
contents = &GophermapContents{ responder.Request, nil }
} else {
contents = &RegularFileContents{ responder.Request.Path, nil }
}
/* Create new file wrapper around 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.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock read mutex then return error */
fs.CacheMutex.RUnlock()
return gophorErr
}
/* 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(responder.Request.Path.Absolute(), 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()
}
/* 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 gophorErr
}
type File struct {
/* Wraps around the cached contents of a file
* helping with management.
*/
Content FileContents
Mutex sync.RWMutex
Fresh bool
LastRefresh int64
}
func (f *File) WriteContents(responder *Responder) *GophorError {
return f.Content.Render(responder)
}
func (f *File) CacheContents() *GophorError {
/* Clear current file contents */
f.Content.Clear()
/* Reload the file */
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
return nil
}
/* Start the file monitor! */
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.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()
/* 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
}
/* Check file still exists on disk, delete and continue if not */
stat, err := os.Stat(path)
if err != nil {
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 is newer, mark as unfresh */
if file.Fresh && file.LastRefresh < timeModified {
file.Fresh = false
}
}
/* Done! We can release cache read lock */
Config.FileSystem.CacheMutex.Unlock()
}
/* Just a helper function to neaten-up checking if file contents is of generated type */
func isGeneratedType(file *File) bool {
switch file.Content.(type) {
case *GeneratedFileContents:
return true
default:
return false
}
}

@ -1,202 +0,0 @@
package main
import (
"os"
"bytes"
"io"
"sort"
"bufio"
)
const (
FileReadBufSize = 1024
)
/* Perform simple buffered read on a file at path */
func bufferedRead(path string) ([]byte, *GophorError) {
/* Open file */
fd, err := os.Open(path)
if err != nil {
return nil, &GophorError{ FileOpenErr, err }
}
defer fd.Close()
/* Setup buffers */
var count int
contents := make([]byte, 0)
buf := make([]byte, FileReadBufSize)
/* Setup reader */
reader := bufio.NewReader(fd)
/* Read through buffer until error or null bytes! */
for {
count, err = reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return nil, &GophorError{ FileReadErr, err }
}
contents = append(contents, buf[:count]...)
if count < FileReadBufSize {
break
}
}
return contents, nil
}
/* Perform buffered read on file at path, then scan through with supplied iterator func */
func bufferedScan(path string, scanIterator func(*bufio.Scanner) bool) *GophorError {
/* First, read raw file contents */
contents, gophorErr := bufferedRead(path)
if gophorErr != nil {
return gophorErr
}
/* Create reader and scanner from this */
reader := bytes.NewReader(contents)
scanner := bufio.NewScanner(reader)
/* If contains DOS line-endings, split by DOS! Else, split by Unix */
if bytes.Contains(contents, []byte(DOSLineEnd)) {
scanner.Split(dosLineEndSplitter)
} else {
scanner.Split(unixLineEndSplitter)
}
/* Scan through file contents using supplied iterator */
for scanner.Scan() && scanIterator(scanner) {}
/* Check scanner finished cleanly */
if scanner.Err() != nil {
return &GophorError{ FileReadErr, scanner.Err() }
}
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 */
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\r\n")); i >= 0 {
/* We have a full new-line terminate line */
return i+2, data[:i], nil
}
/* Request more data */
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 */
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\n")); i >= 0 {
/* We have a full new-line terminate line */
return i+1, data[:i], nil
}
/* Request more data */
return 0, nil, nil
}
/* 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
}
/* 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
}
/* Write the actual directory entry */
gophorErr = listDir(responder, hidden)
if gophorErr != nil {
return gophorErr
}
/* Finally write footer */
return responder.WriteData(Config.FooterText)
}
/* 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(responder.Request.Path.Absolute())
if err != nil {
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.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)
/* Walk through files :D */
var reqPath *RequestPath
for _, file := range files {
reqPath = NewRequestPath(responder.Request.Path.RootDir(), responder.Request.Path.JoinRel(file.Name()))
/* If hidden file, or restricted file, continue! */
if isHiddenFile(hidden, reqPath.Relative()) /*|| isRestrictedFile(reqPath.Relative())*/ {
continue
}
/* 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)
}
/* 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. */
type byName []os.FileInfo
func (s byName) Len() int { return len(s) }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

@ -1,78 +0,0 @@
package main
import (
"container/list"
)
/* TODO: work on efficiency. use our own lower level data structure? */
/* FixedMap:
* A fixed size map that pushes the last
* used value from the stack if size limit
* is reached.
*/
type FixedMap struct {
Map map[string]*MapElement
List *list.List
Size int
}
/* MapElement:
* Simple structure to wrap pointer to list
* element and stored map value together.
*/
type MapElement struct {
Element *list.Element
Value *File
}
func NewFixedMap(size int) *FixedMap {
return &FixedMap{
make(map[string]*MapElement),
list.New(),
size,
}
}
/* Get file in map for key, or nil */
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 */
func (fm *FixedMap) Put(key string, value *File) {
element := fm.List.PushFront(key)
fm.Map[key] = &MapElement{ element, value }
if fm.List.Len() > fm.Size {
/* We're at capacity! SIR! */
element = fm.List.Back()
/* We don't check here as we know this is ALWAYS a string */
key, _ := element.Value.(string)
/* Finally delete the map entry and list element! */
delete(fm.Map, key)
fm.List.Remove(element)
}
}
/* Try delete element, else do nothing */
func (fm *FixedMap) Remove(key string) {
elem, ok := fm.Map[key]
if !ok {
/* We don't have this key, return */
return
}
/* Remove the selected element */
delete(fm.Map, key)
fm.List.Remove(elem.Element)
}

@ -1,50 +0,0 @@
package main
import (
"strings"
)
/* 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)...)
}
}
return append(ret, []byte(LastLine)...)
}
/* 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)
}
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
}
/* Replace new-line characters */
func replaceNewLines(str string) string {
return strings.Replace(str, "\n", "", -1)
}

@ -1,237 +0,0 @@
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
}

@ -0,0 +1,92 @@
package gopher
import "gophor/core"
// Gopher specific error codes
const (
InvalidGophermapErr core.ErrorCode = 1
SubgophermapIsDirErr core.ErrorCode = 2
SubgophermapSizeErr core.ErrorCode = 3
)
// generateErrorMessage returns a message for any gopher specific error codes
func generateErrorMessage(code core.ErrorCode) string {
switch code {
case InvalidGophermapErr:
return invalidGophermapErrStr
case SubgophermapIsDirErr:
return subgophermapIsDirErrStr
case SubgophermapSizeErr:
return subgophermapSizeErrStr
default:
return unknownErrStr
}
}
// generateErrorResponse takes an error code and generates an error response byte slice
func generateErrorResponse(code core.ErrorCode) ([]byte, bool) {
switch code {
case core.ConnWriteErr:
return nil, false // no point responding if we couldn't write
case core.ConnReadErr:
return buildErrorLine(errorResponse503), true
case core.ConnCloseErr:
return nil, false // no point responding if we couldn't close
case core.ListenerResolveErr:
return nil, false // not user facing
case core.ListenerBeginErr:
return nil, false // not user facing
case core.ListenerAcceptErr:
return nil, false // not user facing
case core.InvalidIPErr:
return nil, false // not user facing
case core.InvalidPortErr:
return nil, false // not user facing
case core.FileOpenErr:
return buildErrorLine(errorResponse404), true
case core.FileStatErr:
return buildErrorLine(errorResponse500), true
case core.FileReadErr:
return buildErrorLine(errorResponse500), true
case core.FileTypeErr:
return buildErrorLine(errorResponse404), true
case core.DirectoryReadErr:
return buildErrorLine(errorResponse500), true
case core.RestrictedPathErr:
return buildErrorLine(errorResponse403), true
case core.InvalidRequestErr:
return buildErrorLine(errorResponse400), true
case core.CGIStartErr:
return buildErrorLine(errorResponse500), true
case core.CGIExitCodeErr:
return buildErrorLine(errorResponse500), true
case core.CGIStatus400Err:
return buildErrorLine(errorResponse400), true
case core.CGIStatus401Err:
return buildErrorLine(errorResponse401), true
case core.CGIStatus403Err:
return buildErrorLine(errorResponse403), true
case core.CGIStatus404Err:
return buildErrorLine(errorResponse404), true
case core.CGIStatus408Err:
return buildErrorLine(errorResponse408), true
case core.CGIStatus410Err:
return buildErrorLine(errorResponse410), true
case core.CGIStatus500Err:
return buildErrorLine(errorResponse500), true
case core.CGIStatus501Err:
return buildErrorLine(errorResponse501), true
case core.CGIStatus503Err:
return buildErrorLine(errorResponse503), true
case core.CGIStatusUnknownErr:
return buildErrorLine(errorResponse500), true
case InvalidGophermapErr:
return buildErrorLine(errorResponse500), true
case SubgophermapIsDirErr:
return buildErrorLine(errorResponse500), true
case SubgophermapSizeErr:
return buildErrorLine(errorResponse500), true
default:
return nil, false
}
}

@ -0,0 +1,36 @@
package gopher
import (
"gophor/core"
"os"
)
// gophermapContents is an implementation of core.FileContents that holds individually renderable sections of a gophermap
type gophermapContents struct {
sections []gophermapSection
}
// WriteToClient renders each cached section of the gophermap, and writes them to the client
func (gc *gophermapContents) WriteToClient(client *core.Client, path *core.Path) core.Error {
for _, section := range gc.sections {
err := section.RenderAndWrite(client)
if err != nil {
return err
}
}
// Finally, write the footer (including last-line)
return client.Conn().WriteBytes(footer)
}
// Load takes an open FD and loads the gophermap contents into memory as different renderable sections
func (gc *gophermapContents) Load(fd *os.File, path *core.Path) core.Error {
var err core.Error
gc.sections, err = readGophermap(fd, path)
return err
}
// Clear empties currently cached GophermapContents memory
func (gc *gophermapContents) Clear() {
gc.sections = nil
}

@ -0,0 +1,96 @@
package gopher
import (
"gophor/core"
"os"
"strings"
)
// Gophermap line formatting constants
const (
maxSelectorLen = 255
nullHost = "null.host"
nullPort = "0"
errorSelector = "/error_selector_length"
)
var (
// pageWidth is the maximum set page width of a gophermap document to render to
pageWidth int
// footer holds the formatted footer text (if supplied), and gophermap last-line
footer []byte
)
// formatName formats a gopher line name string
func formatName(name string) string {
if len(name) > pageWidth {
return name[:pageWidth-4] + "...\t"
}
return name + "\t"
}
// formatSelector formats a gopher line selector string
func formatSelector(selector string) string {
if len(selector) > maxSelectorLen {
return errorSelector + "\t"
}
return selector + "\t"
}
// formatHostPort formats a gopher line host + port
func formatHostPort(host, port string) string {
return host + "\t" + port
}
// buildLine builds a gopher line string
func buildLine(t ItemType, name, selector, host, port string) []byte {
return []byte(string(t) + formatName(name) + formatSelector(selector) + formatHostPort(host, port) + "\r\n")
}
// buildInfoLine builds a gopher info line string
func buildInfoLine(line string) []byte {
return []byte(string(typeInfo) + formatName(line) + formatHostPort(nullHost, nullPort) + "\r\n")
}
// buildErrorLine builds a gopher error line string
func buildErrorLine(selector string) []byte {
return []byte(string(typeError) + selector + "\r\n" + ".\r\n")
}
// appendFileListing formats and appends a new file entry as part of a directory listing
func appendFileListing(b []byte, file os.FileInfo, p *core.Path) []byte {
switch {
case file.Mode()&os.ModeDir != 0:
return append(b, buildLine(typeDirectory, file.Name(), p.Selector(), core.Hostname, core.FwdPort)...)
case file.Mode()&os.ModeType == 0:
t := getItemType(p.Relative())
return append(b, buildLine(t, file.Name(), p.Selector(), core.Hostname, core.FwdPort)...)
default:
return b
}
}
// buildFooter formats a raw gopher footer ready to attach to end of gophermaps (including DOS line-end)
func buildFooter(raw string) []byte {
ret := make([]byte, 0)
if raw != "" {
ret = append(ret, buildInfoLine(footerLineSeparator())...)
for _, line := range strings.Split(raw, "\n") {
ret = append(ret, buildInfoLine(line)...)
}
}
return append(ret, []byte(".\r\n")...)
}
// footerLineSeparator is an internal function that generates a footer line separator string
func footerLineSeparator() string {
ret := ""
for i := 0; i < pageWidth; i++ {
ret += "_"
}
return ret
}

@ -0,0 +1,240 @@
package gopher
import (
"gophor/core"
"os"
)
var (
// subgophermapSizeMax specifies the maximum size of an included subgophermap
subgophermapSizeMax int64
)
// GophermapSection is an interface that specifies individually renderable (and writeable) sections of a gophermap
type gophermapSection interface {
RenderAndWrite(*core.Client) core.Error
}
// readGophermap reads a FD and Path as gophermap sections
func readGophermap(fd *os.File, p *core.Path) ([]gophermapSection, core.Error) {
// Create return slice
sections := make([]gophermapSection, 0)
// Create hidden files map now in case later requested
hidden := map[string]bool{
p.Relative(): true,
}
// Error setting within nested function below
var returnErr core.Error
// Perform scan of gophermap FD
titleAlready := false
scanErr := core.FileSystem.ScanFile(
fd,
func(line string) bool {
// Parse the line item type and handle
lineType := parseLineType(line)
switch lineType {
case typeInfoNotStated:
// Append TypeInfo to beginning of line
sections = append(sections, &TextSection{buildInfoLine(line)})
return true
case typeTitle:
// Reformat title line to send as info line with appropriate selector
if !titleAlready {
sections = append(sections, &TextSection{buildLine(typeInfo, line[1:], "TITLE", nullHost, nullPort)})
titleAlready = true
return true
}
returnErr = core.NewError(InvalidGophermapErr)
return false
case typeComment:
// ignore this line
return true
case typeHiddenFile:
// Add to hidden files map
hidden[p.JoinRelative(line[1:])] = true
return true
case typeSubGophermap:
// Parse new Path and parameters
request := core.ParseInternalRequest(p, line[1:])
if returnErr != nil {
return false
} else if request.Path().Relative() == "" || request.Path().Relative() == p.Relative() {
returnErr = core.NewError(InvalidGophermapErr)
return false
}
// Open FD
var subFD *os.File
subFD, returnErr = core.FileSystem.OpenFile(request.Path())
if returnErr != nil {
return false
}
// Get stat
stat, err := subFD.Stat()
if err != nil {
returnErr = core.WrapError(core.FileStatErr, err)
return false
} else if stat.IsDir() {
returnErr = core.NewError(SubgophermapIsDirErr)
return false
}
// Handle CGI script
if core.WithinCGIDir(request.Path()) {
sections = append(sections, &CGISection{request})
return true
}
// Error out if file too big
if stat.Size() > subgophermapSizeMax {
returnErr = core.NewError(SubgophermapSizeErr)
return false
}
// Handle regular file
if !isGophermap(request.Path()) {
sections = append(sections, &FileSection{})
return true
}
// Handle gophermap
sections = append(sections, &SubgophermapSection{})
return true
case typeEnd:
// Last line, break-out!
return false
case typeEndBeginList:
// Append DirectorySection object then break, as-with typeEnd
dirPath := p.Dir()
sections = append(sections, &DirectorySection{hidden, dirPath})
return false
default:
// Default is appending to sections slice as TextSection
sections = append(sections, &TextSection{[]byte(line + "\r\n")})
return true
}
},
)
// Check the scan didn't exit with error
if returnErr != nil {
return nil, returnErr
} else if scanErr != nil {
return nil, scanErr
}
return sections, nil
}
// TextSection is a simple implementation that holds line's byte contents as-is
type TextSection struct {
contents []byte
}
// RenderAndWrite simply writes the byte slice to the client
func (s *TextSection) RenderAndWrite(client *core.Client) core.Error {
return client.Conn().WriteBytes(s.contents)
}
// DirectorySection is an implementation that holds a dir path, and map of hidden files, to later list a dir contents
type DirectorySection struct {
hidden map[string]bool
path *core.Path
}
// RenderAndWrite scans and renders a list of the contents of a directory (skipping hidden or restricted files)
func (s *DirectorySection) RenderAndWrite(client *core.Client) core.Error {
fd, err := core.FileSystem.OpenFile(s.path)
if err != nil {
return err
}
// Slice to write
dirContents := make([]byte, 0)
// Scan directory and build lines
err = core.FileSystem.ScanDirectory(fd, s.path, func(file os.FileInfo, p *core.Path) {
// Append new formatted file listing (if correct type)
dirContents = appendFileListing(dirContents, file, p)
})
if err != nil {
return err
}
// Write dirContents to client
return client.Conn().WriteBytes(dirContents)
}
// FileSection is an implementation that holds a file path, and writes the file contents to client
type FileSection struct {
path *core.Path
}
// RenderAndWrite simply opens, reads and writes the file contents to the client
func (s *FileSection) RenderAndWrite(client *core.Client) core.Error {
// Open FD for the file
fd, err := core.FileSystem.OpenFile(s.path)
if err != nil {
return err
}
// Read the file contents into memory
b, err := core.FileSystem.ReadFile(fd)
if err != nil {
return err
}
// Write the file contents to the client
return client.Conn().WriteBytes(b)
}
// SubgophermapSection is an implementation to hold onto a gophermap path, then read, render and write contents to a client
type SubgophermapSection struct {
path *core.Path
}
// RenderAndWrite reads, renders and writes the contents of the gophermap to the client
func (s *SubgophermapSection) RenderAndWrite(client *core.Client) core.Error {
// Get FD for gophermap
fd, err := core.FileSystem.OpenFile(s.path)
if err != nil {
return err
}
// Read gophermap into sections
sections, err := readGophermap(fd, s.path)
if err != nil {
return err
}
// Write each of the sections (AAAA COULD BE RECURSIONNNNN)
for _, section := range sections {
err := section.RenderAndWrite(client)
if err != nil {
return err
}
}
return nil
}
// CGISection is an implementation that holds onto a built request, then processing as a CGI request on request
type CGISection struct {
request *core.Request
}
// RenderAndWrite takes the request, and executes the associated CGI script with parameters
func (s *CGISection) RenderAndWrite(client *core.Client) core.Error {
return core.ExecuteCGIScript(client, s.request)
}

@ -0,0 +1,22 @@
package gopher
// generateHTMLRedirect takes a URL string and generates an HTML redirect page bytes
func generateHTMLRedirect(url string) []byte {
content :=
"<html>\n" +
"<head>\n" +
"<meta http-equiv=\"refresh\" content=\"1;URL=" + url + "\">" +
"</head>\n" +
"<body>\n" +
"You are following an external link to a web site.\n" +
"You will be automatically taken to the site shortly.\n" +
"If you do not get sent there, please click <A HREF=\"" + url + "\">here</A> to go to the web site.\n" +
"<p>\n" +
"The URL linked is <A HREF=\"" + url + "\">" + url + "</A>\n" +
"<p>\n" +
"Thanks for using Gophor!\n" +
"</body>\n" +
"</html>\n"
return []byte(content)
}

@ -0,0 +1,192 @@
package gopher
import "strings"
// ItemType specifies a gopher item type char
type ItemType byte
// RFC 1436 Standard item types
const (
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 item types
const (
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 item types
const (
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 */
)
// Internal item types
const (
typeDefault = typeBin
typeInfoNotStated = ItemType('I')
typeUnknown = ItemType('?')
)
// fileExtMap specifies mapping of file extensions to gopher item types
var fileExtMap = map[string]ItemType{
".out": typeBin,
".a": typeBin,
".o": typeBin,
".ko": typeBin, /* Kernel extensions... WHY ARE YOU GIVING ACCESS TO DIRECTORIES WITH THIS */
".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,
".avi": typeVideo,
}
// getItemType is an internal function to get an ItemType for a file name string
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't tell
return typeDefault
default:
// get index of str after last '.', look up in fileExtMap
fileType, ok := fileExtMap["."+split[splitLen-1]]
if ok {
return fileType
}
return typeDefault
}
}
// parseLineType parses a gophermap's line type based on first char and contents
func parseLineType(line string) ItemType {
lineLen := len(line)
if lineLen == 0 {
return typeInfoNotStated
}
// Get ItemType for first char
t := ItemType(line[0])
if lineLen == 1 {
// The only accepted types for length 1 line below:
t := ItemType(line[0])
if t == typeEnd ||
t == typeEndBeginList ||
t == typeComment ||
t == typeInfo ||
t == typeTitle {
return t
}
return typeUnknown
} else if !strings.Contains(line, "\t") {
// The only accepted types for length >= 1 and with a tab
if t == typeComment ||
t == typeTitle ||
t == typeInfo ||
t == typeHiddenFile ||
t == typeSubGophermap {
return t
}
return typeInfoNotStated
}
return t
}

@ -0,0 +1,37 @@
package gopher
import (
"flag"
"gophor/core"
)
// setup parses gopher specific flags, and all core flags, preparing server for .Run()
func setup() {
pWidth := flag.Uint(pageWidthFlagStr, 80, pageWidthDescStr)
footerText := flag.String(footerTextFlagStr, "Gophor, a gopher server in Go!", footerTextDescStr)
subgopherSizeMax := flag.Float64(subgopherSizeMaxFlagStr, 1.0, subgopherSizeMaxDescStr)
admin := flag.String(adminFlagStr, "", adminDescStr)
desc := flag.String(descFlagStr, "", descDescStr)
geo := flag.String(geoFlagStr, "", geoDescStr)
core.ParseFlagsAndSetup(generateErrorMessage)
// Setup gopher specific global variables
subgophermapSizeMax = int64(1048576.0 * *subgopherSizeMax) // convert float to megabytes
pageWidth = int(*pWidth)
footer = buildFooter(*footerText)
gophermapRegex = compileGophermapRegex()
// Generate capability files
capsTxt := generateCapsTxt(*desc, *admin, *geo)
robotsTxt := generateRobotsTxt()
// Add generated files to cache
core.FileSystem.AddGeneratedFile(core.NewPath(core.Root, "caps.txt"), capsTxt)
core.FileSystem.AddGeneratedFile(core.NewPath(core.Root, "robots.txt"), robotsTxt)
}
// Run does as says :)
func Run() {
setup()
core.Start(serve)
}

@ -0,0 +1,48 @@
package gopher
import "gophor/core"
func generatePolicyHeader(name string) string {
text := "# This is an automatically generated" + "\r\n"
text += "# server policy file: " + name + "\r\n"
text += "#" + "\r\n"
text += "# BlackLivesMatter" + "\r\n"
return text
}
func generateCapsTxt(desc, admin, geo string) []byte {
text := "CAPS" + "\r\n"
text += "\r\n"
text += generatePolicyHeader("caps.txt")
text += "\r\n"
text += "CapsVersion=1" + "\r\n"
text += "ExpireCapsAfter=1800" + "\r\n"
text += "\r\n"
text += "PathDelimeter=/" + "\r\n"
text += "PathIdentity=." + "\r\n"
text += "PathParent=.." + "\r\n"
text += "PathParentDouble=FALSE" + "\r\n"
text += "PathEscapeCharacter=\\" + "\r\n"
text += "PathKeepPreDelimeter=FALSE" + "\r\n"
text += "\r\n"
text += "ServerSoftware=Gophor" + "\r\n"
text += "ServerSoftwareVersion=" + core.Version + "\r\n"
text += "ServerDescription=" + desc + "\r\n"
text += "ServerGeolocationString=" + geo + "\r\n"
text += "ServerDefaultEncoding=utf-8" + "\r\n"
text += "\r\n"
text += "ServerAdmin=" + admin + "\r\n"
return []byte(text)
}
func generateRobotsTxt() []byte {
text := generatePolicyHeader("robots.txt")
text += "\r\n"
text += "Usage-agent: *" + "\r\n"
text += "Disallow: *" + "\r\n"
text += "\r\n"
text += "Crawl-delay: 99999" + "\r\n"
text += "\r\n"
text += "# This server does not support scraping" + "\r\n"
return []byte(text)
}

@ -0,0 +1,21 @@
package gopher
import (
"gophor/core"
"regexp"
)
var (
// gophermapRegex is the precompiled gophermap file name regex check
gophermapRegex *regexp.Regexp
)
// compileGophermapRegex compiles the gophermap file name check regex
func compileGophermapRegex() *regexp.Regexp {
return regexp.MustCompile(`^(|.+/|.+\.)gophermap$`)
}
// isGophermap checks against gophermap regex as to whether a file path is a gophermap
func isGophermap(path *core.Path) bool {
return gophermapRegex.MatchString(path.Relative())
}

@ -0,0 +1,110 @@
package gopher
import (
"gophor/core"
"os"
"strings"
)
// serve is the global gopher server's serve function
func serve(client *core.Client) {
// Receive line from client
received, err := client.Conn().ReadLine()
if err != nil {
client.LogError(clientReadFailStr)
handleError(client, err)
return
}
// Convert to string
line := string(received)
// If prefixed by 'URL:' send a redirect
lenBefore := len(line)
line = strings.TrimPrefix(line, "URL:")
if len(line) < lenBefore {
client.Conn().WriteBytes(generateHTMLRedirect(line))
client.LogInfo(clientRedirectFmtStr, line)
return
}
// Parse new request
request, err := core.ParseURLEncodedRequest(line)
if err != nil {
client.LogError(clientRequestParseFailStr)
handleError(client, err)
return
}
// Handle the request!
err = core.FileSystem.HandleClient(
client,
request,
newFileContents,
func(fs *core.FileSystemObject, client *core.Client, fd *os.File, p *core.Path) core.Error {
// First check for gophermap, create gophermap Path object
gophermap := p.JoinPath("gophermap")
// If gophermap exists, we fetch this
fd2, err := fs.OpenFile(gophermap)
if err == nil {
stat, osErr := fd2.Stat()
if osErr == nil {
return fs.FetchFile(client, fd2, stat, gophermap, newFileContents)
}
// Else, just close fd2
fd2.Close()
}
// Slice to write
dirContents := make([]byte, 0)
// Add directory heading + empty line
dirContents = append(dirContents, buildLine(typeInfo, "[ "+core.Hostname+p.Selector()+" ]", "TITLE", nullHost, nullPort)...)
dirContents = append(dirContents, buildInfoLine("")...)
// Scan directory and build lines
err = fs.ScanDirectory(
fd,
p,
func(file os.FileInfo, fp *core.Path) {
// Append new formatted file listing (if correct type)
dirContents = appendFileListing(dirContents, file, fp)
},
)
if err != nil {
return err
}
// Add footer, write contents
dirContents = append(dirContents, footer...)
return client.Conn().WriteBytes(dirContents)
},
)
// Final error handling
if err != nil {
handleError(client, err)
client.LogError(clientServeFailStr, request.Path().Absolute())
} else {
client.LogInfo(clientServedStr, request.Path().Absolute())
}
}
// handleError determines whether to send an error response to the client, and logs to system
func handleError(client *core.Client, err core.Error) {
response, ok := generateErrorResponse(err.Code())
if ok {
client.Conn().WriteBytes(response)
}
core.SystemLog.Error(err.Error())
}
// newFileContents returns a new FileContents object
func newFileContents(p *core.Path) core.FileContents {
if isGophermap(p) {
return &gophermapContents{}
}
return &core.RegularFileContents{}
}

@ -0,0 +1,49 @@
package gopher
// Client error response strings
const (
errorResponse400 = "400 Bad Request"
errorResponse401 = "401 Unauthorised"
errorResponse403 = "403 Forbidden"
errorResponse404 = "404 Not Found"
errorResponse408 = "408 Request Time-out"
errorResponse410 = "410 Gone"
errorResponse500 = "500 Internal Server Error"
errorResponse501 = "501 Not Implemented"
errorResponse503 = "503 Service Unavailable"
)
// Gopher flag string constants
const (
pageWidthFlagStr = "page-width"
pageWidthDescStr = "Gopher page width"
footerTextFlagStr = "footer-text"
footerTextDescStr = "Footer text (empty to disable)"
subgopherSizeMaxFlagStr = "subgopher-size-max"
subgopherSizeMaxDescStr = "Subgophermap size max (megabytes)"
adminFlagStr = "admin"
adminDescStr = "Generated policy file admin email"
descFlagStr = "description"
descDescStr = "Generated policy file server description"
geoFlagStr = "geolocation"
geoDescStr = "Generated policy file server geolocation"
)
// Log string constants
const (
clientReadFailStr = "Failed to read"
clientRedirectFmtStr = "Redirecting to: %s"
clientRequestParseFailStr = "Failed to parse request"
clientServeFailStr = "Failed to serve: %s"
clientServedStr = "Served: %s"
invalidGophermapErrStr = "Invalid gophermap"
subgophermapIsDirErrStr = "Subgophermap path is dir"
subgophermapSizeErrStr = "Subgophermap size too large"
unknownErrStr = "Unknown error code"
)

@ -1,254 +0,0 @@
package main
import (
"os"
"log"
"strconv"
"syscall"
"os/signal"
"flag"
"time"
)
const (
GophorVersion = "1.0-beta"
)
var (
Config *ServerConfig
)
func main() {
/* Quickly setup global logger */
setupGlobalLogger()
/* Setup the entire server, getting slice of listeners in return */
listeners := setupServer()
/* Handle signals so we can _actually_ shutdowm */
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
/* Start accepting connections on any supplied listeners */
for _, l := range listeners {
go func() {
Config.SysLog.Info("", "Listening on: gopher://%s:%s\n", l.Host.Name(), l.Host.RealPort())
for {
worker, err := l.Accept()
if err != nil {
Config.SysLog.Error("", "Error accepting connection: %s\n", err.Error())
continue
}
go worker.Serve()
}
}()
}
/* When OS signal received, we close-up */
sig := <-signals
Config.SysLog.Info("", "Signal received: %v. Shutting down...\n", sig)
os.Exit(0)
}
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.")
serverBindAddr := flag.String("bind-addr", "", "Change server socket bind address")
serverPort := flag.Int("port", 70, "Change server bind port.")
serverFwdPort := flag.Int("fwd-port", 0, "Change port used in '$port' replacement strings (useful if you're port forwarding).")
serverHostname := flag.String("hostname", "", "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 */
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
// charSet := flag.String("charset", "", "Change default output charset.")
charSet := "utf-8"
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.")
/* 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")
/* 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.")
/* Parse parse parse!! */
flag.Parse()
if *version {
printVersionExit()
}
/* If hostname is nil we set it to bind-addr */
if *serverHostname == "" {
/* If both are blank that ain't too helpful */
if *serverBindAddr == "" {
log.Fatalf("Cannot have both -bind-addr and -hostname as empty!\n")
} else {
*serverHostname = *serverBindAddr
}
}
/* Setup the server configuration instance and enter as much as we can right now */
Config = new(ServerConfig)
/* 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)
/* Setup Gophor logging system */
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 {
/* 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
}
/* 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.SysLog.Info("", "Entered server directory: %s\n", *serverRoot)
/* Setup listeners */
listeners := make([]*GophorListener, 0)
/* If requested, setup unencrypted listener */
if *serverPort != 0 {
/* 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 {
log.Fatalf("Error setting up (unencrypted) listener: %s\n", err.Error())
}
listeners = append(listeners, l)
} else {
log.Fatalf("No valid port to listen on\n")
}
/* Setup file cache */
Config.FileSystem = new(FileSystem)
/* Check if cache requested disabled */
if !*cacheDisabled {
/* Init file cache */
Config.FileSystem.Init(*cacheSize, *cacheFileSizeMax)
/* Before file monitor or any kind of new goroutines started,
* check if we need to cache generated policy files
*/
cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
/* Start file cache freshness checker */
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.SysLog.Info("", "File caching disabled\n")
/* Safe to cache policy files now */
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
}
func enterServerDir(path string) {
err := syscall.Chdir(path)
if err != nil {
log.Fatalf("Error changing dir to server root %s: %s\n", path, err.Error())
}
}

@ -1,22 +0,0 @@
package main
/* Function does, as function is named */
func generateHtmlRedirect(url string) []byte {
content :=
"<html>\n"+
"<head>\n"+
"<meta http-equiv=\"refresh\" content=\"1;URL="+url+"\">"+
"</head>\n"+
"<body>\n"+
"You are following an external link to a web site.\n"+
"You will be automatically taken to the site shortly.\n"+
"If you do not get sent there, please click <A HREF=\""+url+"\">here</A> to go to the web site.\n"+
"<p>\n"+
"The URL linked is <A HREF=\""+url+"\">"+url+"</A>\n"+
"<p>\n"+
"Thanks for using Gophor!\n"+
"</body>\n"+
"</html>\n"
return []byte(content)
}

@ -1,209 +0,0 @@
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
}
}

@ -1,157 +0,0 @@
package main
import (
"log"
"os"
"strings"
)
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 global logger */
func setupGlobalLogger() {
log.SetFlags(0)
log.SetOutput(os.Stderr)
}
/* 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
case LogToStderr:
/* Return two separate stderr loggers */
sysLogger := &LoggerNoPrefix{ NewLoggerToStderr(logFlags) }
if logIps {
return sysLogger, &Logger{ NewLoggerToStderr(logFlags) }
} else {
return sysLogger, &LoggerNoPrefix{ NewLoggerToStderr(logFlags) }
}
case LogDisabled:
/* Return two pointers to same null logger */
nullLogger := &NullLogger{}
return nullLogger, nullLogger
case LogToFile:
/* Return two separate file loggers */
sysLogger := &Logger{ NewLoggerToFile(systemLogPath, logFlags) }
if logIps {
return sysLogger, &Logger{ NewLoggerToFile(accessLogPath, logFlags) }
} else {
return sysLogger, &LoggerNoPrefix{ NewLoggerToFile(accessLogPath, logFlags) }
}
default:
log.Fatalf("Unrecognised log output type: %s\n", logOutput)
return nil, nil
}
}
/* 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() {
log.SetFlags(0)
log.Printf("%s\n", GophorVersion)
os.Exit(0)
}

@ -0,0 +1,9 @@
package main
import (
"gophor/gopher"
)
func main() {
gopher.Run()
}

@ -1,154 +0,0 @@
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
}

@ -1,96 +0,0 @@
package main
import (
"os"
"path"
"sync"
)
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(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 := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */
file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put(rootDir+"/"+CapsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+CapsTxtStr)
}
/* See if robots txt exists, if not generate */
_, err = os.Stat(rootDir+"/"+RobotsTxtStr)
if err != nil {
/* 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 := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */
file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */
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 += generatePolicyHeader(CapsTxtStr)
text += DOSLineEnd
text += "CapsVersion=1"+DOSLineEnd
text += "ExpireCapsAfter=1800"+DOSLineEnd
text += DOSLineEnd
text += "PathDelimeter=/"+DOSLineEnd
text += "PathIdentity=."+DOSLineEnd
text += "PathParent=.."+DOSLineEnd
text += "PathParentDouble=FALSE"+DOSLineEnd
text += "PathEscapeCharacter=\\"+DOSLineEnd
text += "PathKeepPreDelimeter=FALSE"+DOSLineEnd
text += DOSLineEnd
text += "ServerSoftware=Gophor"+DOSLineEnd
text += "ServerSoftwareVersion="+GophorVersion+DOSLineEnd
text += "ServerDescription="+description+DOSLineEnd
text += "ServerGeolocationString="+geoloc+DOSLineEnd
// text += "ServerDefaultEncoding=ascii"+DOSLineEnd
text += DOSLineEnd
text += "ServerAdmin="+admin+DOSLineEnd
return []byte(text)
}
func generateRobotsTxt() []byte {
text := generatePolicyHeader(RobotsTxtStr)
text += DOSLineEnd
text += "Usage-agent: *"+DOSLineEnd
text += "Disallow: *"+DOSLineEnd
text += DOSLineEnd
text += "Crawl-delay: 99999"+DOSLineEnd
text += DOSLineEnd
text += "# This server does not support scraping"+DOSLineEnd
return []byte(text)
}

@ -1,94 +0,0 @@
package main
import (
"regexp"
"strings"
"log"
)
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 */
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
}
/* Try compile regex */
regex, err := regexp.Compile(expr)
if err != nil {
log.Fatalf("Failed compiling user supplied regex: %s\n", expr)
}
/* Append restricted */
restrictedRegex = append(restrictedRegex, regex)
Config.SysLog.Info("", "Compiled restricted: %s\n", expr)
}
return restrictedRegex
}
/* 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 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)
}

@ -1,130 +0,0 @@
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
}

@ -1,54 +0,0 @@
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,71 +0,0 @@
package main
import (
"strings"
)
type Worker struct {
Conn *BufferedDeadlineConn
Host *ConnHost
Client *ConnClient
RootDir string
}
func (worker *Worker) Serve() {
defer worker.Conn.Close()
line, err := worker.Conn.ReadLine()
if err != nil {
Config.SysLog.Error("", "Error reading from socket port %s: %s\n", worker.Host.Port(), err.Error())
return
}
/* Drop up to first tab */
received := strings.Split(string(line), Tab)[0]
/* Handle URL request if presented */
lenBefore := len(received)
received = strings.TrimPrefix(received, "URL:")
switch len(received) {
case lenBefore-4:
/* Send an HTML redirect to supplied URL */
Config.AccLog.Info("("+worker.Client.Ip()+") ", "Redirecting to %s\n", received)
worker.Conn.Write(generateHtmlRedirect(received))
return
default:
/* Do nothing */
}
/* Create GopherUrl object from request string */
url, gophorErr := parseGopherUrl(received)
if gophorErr == nil {
/* Create new request from url object */
request := NewSanitizedRequest(worker.RootDir, url)
/* Create new responder from request */
responder := NewResponder(worker.Conn, worker.Host, worker.Client, request)
/* 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())
}
}
/* Log serve failure to error to system */
Config.SysLog.Error("", gophorErr.Error())
/* Generate response bytes from error code */
errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
/* 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)
}
}
Loading…
Cancel
Save