commit
9b18a9caeb
@ -1,38 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
PROJECT='gophor'
|
|
||||||
OUTDIR='build'
|
|
||||||
VERSION="$(cat 'constants.go' | grep -E '^\s*GophorVersion' | sed -e 's|\s*GophorVersion = \"||' -e 's|\"\s*$||')"
|
|
||||||
GOVERSION="$(go version | sed -e 's|^go version go||' -e 's|\s.*$||')"
|
|
||||||
LOGFILE='build.log'
|
|
||||||
|
|
||||||
silent() {
|
|
||||||
"$@" > "$LOGFILE" 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
build_for() {
|
|
||||||
local archname="$1" toolchain="$2" os="$3" arch="$4"
|
|
||||||
shift 4
|
|
||||||
if [ "$arch" = 'arm' ]; then
|
|
||||||
local armversion="$1"
|
|
||||||
shift 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building for ${os} ${archname}..."
|
|
||||||
local filename="${OUTDIR}/${PROJECT}_${os}_${archname}"
|
|
||||||
CGO_ENABLED=1 CC="$toolchain" GOOS="$os" GOARCH="$arch" GOARM="$armversion" silent go build -trimpath -o "$filename" "$@"
|
|
||||||
if [ "$?" -ne 0 ]; then
|
|
||||||
echo "Failed!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Compressing ${filename}..."
|
|
||||||
silent upx --best "$filename"
|
|
||||||
silent upx -t "$filename"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build time :)
|
|
||||||
build_for 'amd64' 'x86_64-linux-musl-gcc' 'linux' 'amd64' -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
|
|
@ -1,97 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
const (
|
|
||||||
/* Gophor */
|
|
||||||
GophorVersion = "0.5-alpha"
|
|
||||||
|
|
||||||
/* Socket settings */
|
|
||||||
SocketReadBufSize = 256 /* Supplied selector shouldn't be longer than this anyways */
|
|
||||||
MaxSocketReadChunks = 1
|
|
||||||
FileReadBufSize = 1024
|
|
||||||
|
|
||||||
/* Parsing */
|
|
||||||
DOSLineEnd = "\r\n"
|
|
||||||
UnixLineEnd = "\n"
|
|
||||||
|
|
||||||
End = "."
|
|
||||||
Tab = "\t"
|
|
||||||
LastLine = End+DOSLineEnd
|
|
||||||
|
|
||||||
/* Line creation */
|
|
||||||
MaxUserNameLen = 70 /* RFC 1436 standard */
|
|
||||||
MaxSelectorLen = 255 /* RFC 1436 standard */
|
|
||||||
|
|
||||||
NullSelector = "-"
|
|
||||||
NullHost = "null.host"
|
|
||||||
NullPort = "0"
|
|
||||||
|
|
||||||
SelectorErrorStr = "selector_length_error"
|
|
||||||
GophermapRenderErrorStr = ""
|
|
||||||
|
|
||||||
/* Replacement strings */
|
|
||||||
ReplaceStrHostname = "$hostname"
|
|
||||||
ReplaceStrPort = "$port"
|
|
||||||
|
|
||||||
/* Filesystem */
|
|
||||||
GophermapFileStr = "gophermap"
|
|
||||||
CapsTxtStr = "caps.txt"
|
|
||||||
RobotsTxtStr = "robots.txt"
|
|
||||||
|
|
||||||
/* Misc */
|
|
||||||
BytesInMegaByte = 1048576.0
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Item type characters:
|
|
||||||
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
|
|
||||||
* and Gophernicus project. Those with ALL-CAPS descriptions in
|
|
||||||
* [square brackets] defined and used by Gophernicus, a popular
|
|
||||||
* Gopher server.
|
|
||||||
*/
|
|
||||||
type ItemType byte
|
|
||||||
const (
|
|
||||||
/* RFC 1436 Standard */
|
|
||||||
TypeFile = ItemType('0') /* Regular file (text) */
|
|
||||||
TypeDirectory = ItemType('1') /* Directory (menu) */
|
|
||||||
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
|
|
||||||
TypeError = ItemType('3') /* Error message */
|
|
||||||
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
|
|
||||||
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
|
|
||||||
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
|
|
||||||
TypeSearch = ItemType('7') /* Query search engine or CGI script */
|
|
||||||
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
|
|
||||||
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
|
|
||||||
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
|
|
||||||
TypeGif = ItemType('g') /* GIF format image file (just use I) */
|
|
||||||
TypeImage = ItemType('I') /* Any format image file */
|
|
||||||
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
|
|
||||||
|
|
||||||
/* GopherII Standard */
|
|
||||||
TypeCalendar = ItemType('c') /* Calendar file */
|
|
||||||
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
|
|
||||||
TypeHtml = ItemType('h') /* HTML document */
|
|
||||||
TypeInfo = ItemType('i') /* Informational text (not selectable) */
|
|
||||||
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
|
|
||||||
TypeMail = ItemType('M') /* Email repository (MBOX) */
|
|
||||||
TypeAudio = ItemType('s') /* Audio recordings */
|
|
||||||
TypeXml = ItemType('x') /* eXtensible Markup Language document */
|
|
||||||
TypeVideo = ItemType(';') /* Video files */
|
|
||||||
|
|
||||||
/* Commonly Used */
|
|
||||||
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
|
|
||||||
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
|
|
||||||
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
|
|
||||||
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
|
|
||||||
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
|
|
||||||
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on a directory listing */
|
|
||||||
|
|
||||||
/* Planned To Be Supported */
|
|
||||||
TypeExec = ItemType('$') /* [SERVER ONLY] Execute shell command and print stdout here */
|
|
||||||
|
|
||||||
/* Default type */
|
|
||||||
TypeDefault = TypeBin
|
|
||||||
|
|
||||||
/* Gophor specific types */
|
|
||||||
TypeInfoNotStated = ItemType('z') /* [INTERNAL USE] */
|
|
||||||
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
|
|
||||||
)
|
|
@ -0,0 +1,160 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Setup initial (i.e. constant) gophermap / command environment variables */
|
||||||
|
func setupExecEnviron(path string) []string {
|
||||||
|
return []string {
|
||||||
|
envKeyValue("PATH", path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup initial (i.e. constant) CGI environment variables */
|
||||||
|
func setupInitialCgiEnviron(path, charset string) []string {
|
||||||
|
return []string{
|
||||||
|
/* RFC 3875 standard */
|
||||||
|
envKeyValue("GATEWAY_INTERFACE", "CGI/1.1"), /* MUST be set to the dialect of CGI being used by the server */
|
||||||
|
envKeyValue("SERVER_SOFTWARE", "gophor/"+GophorVersion), /* MUST be set to name and version of server software serving this request */
|
||||||
|
envKeyValue("SERVER_PROTOCOL", "gopher"), /* MUST be set to name and version of application protocol used for this request */
|
||||||
|
envKeyValue("CONTENT_LENGTH", "0"), /* Contains size of message-body attached (always 0 so we set here) */
|
||||||
|
envKeyValue("REQUEST_METHOD", "GET"), /* MUST be set to method by which script should process request. Always GET */
|
||||||
|
|
||||||
|
/* Non-standard */
|
||||||
|
envKeyValue("PATH", path),
|
||||||
|
envKeyValue("COLUMNS", strconv.Itoa(Config.PageWidth)),
|
||||||
|
envKeyValue("GOPHER_CHARSET", charset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generate CGI environment */
|
||||||
|
func generateCgiEnvironment(responder *Responder) []string {
|
||||||
|
/* Get initial CgiEnv variables */
|
||||||
|
env := Config.CgiEnv
|
||||||
|
|
||||||
|
env = append(env, envKeyValue("SERVER_NAME", responder.Host.Name())) /* MUST be set to name of server host client is connecting to */
|
||||||
|
env = append(env, envKeyValue("SERVER_PORT", responder.Host.Port())) /* MUST be set to the server port that client is connecting to */
|
||||||
|
env = append(env, envKeyValue("REMOTE_ADDR", responder.Client.Ip())) /* Remote client addr, MUST be set */
|
||||||
|
env = append(env, envKeyValue("QUERY_STRING", responder.Request.Parameters)) /* URL encoded search or parameter string, MUST be set even if empty */
|
||||||
|
env = append(env, envKeyValue("SCRIPT_NAME", "/"+responder.Request.Path.Relative())) /* URI path (not URL encoded) which could identify the CGI script (rather than script's output) */
|
||||||
|
env = append(env, envKeyValue("SCRIPT_FILENAME", responder.Request.Path.Absolute())) /* Basically SCRIPT_NAME absolute path */
|
||||||
|
env = append(env, envKeyValue("SELECTOR", responder.Request.Path.Selector()))
|
||||||
|
env = append(env, envKeyValue("DOCUMENT_ROOT", responder.Request.Path.RootDir()))
|
||||||
|
env = append(env, envKeyValue("REQUEST_URI", "/"+responder.Request.Path.Relative()+responder.Request.Parameters))
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute a CGI script (pointer to correct function) */
|
||||||
|
var executeCgi func(*Responder) *GophorError
|
||||||
|
|
||||||
|
/* Execute CGI script and serve as-is */
|
||||||
|
func executeCgiNoHttp(responder *Responder) *GophorError {
|
||||||
|
return execute(responder.Conn, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute CGI script and strip HTTP headers */
|
||||||
|
func executeCgiStripHttp(responder *Responder) *GophorError {
|
||||||
|
/* HTTP header stripping writer that also parses HTTP status codes */
|
||||||
|
httpStripWriter := NewHttpStripWriter(responder.Conn)
|
||||||
|
|
||||||
|
/* Execute the CGI script using the new httpStripWriter */
|
||||||
|
gophorErr := execute(httpStripWriter, generateCgiEnvironment(responder), responder.Request.Path.Absolute())
|
||||||
|
|
||||||
|
/* httpStripWriter's error takes priority as it might have parsed the status code */
|
||||||
|
cgiStatusErr := httpStripWriter.FinishUp()
|
||||||
|
if cgiStatusErr != nil {
|
||||||
|
return cgiStatusErr
|
||||||
|
} else {
|
||||||
|
return gophorErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute any file (though only allowed are gophermaps) */
|
||||||
|
func executeFile(responder *Responder) *GophorError {
|
||||||
|
return execute(responder.Conn, Config.Env, responder.Request.Path.Absolute())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Execute a supplied path with arguments and environment, to writer */
|
||||||
|
func execute(writer io.Writer, env []string, path string) *GophorError {
|
||||||
|
/* If CGI disbabled, just return error */
|
||||||
|
if !Config.CgiEnabled {
|
||||||
|
return &GophorError{ CgiDisabledErr, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup command */
|
||||||
|
cmd := exec.Command(path)
|
||||||
|
|
||||||
|
/* Set new proccess group id */
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
|
/* Setup cmd env */
|
||||||
|
cmd.Env = env
|
||||||
|
|
||||||
|
/* Setup out buffer */
|
||||||
|
cmd.Stdout = writer
|
||||||
|
|
||||||
|
/* Start executing! */
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return &GophorError{ CommandStartErr, err }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Setup timer goroutine to kill cmd after x time */
|
||||||
|
go func() {
|
||||||
|
time.Sleep(Config.MaxExecRunTime)
|
||||||
|
|
||||||
|
if cmd.ProcessState != nil {
|
||||||
|
/* We've already finished */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get process group id */
|
||||||
|
pgid, err := syscall.Getpgid(cmd.Process.Pid)
|
||||||
|
if err != nil {
|
||||||
|
Config.SysLog.Fatal("", "Process unfinished, PGID not found!\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kill process group! */
|
||||||
|
err = syscall.Kill(-pgid, syscall.SIGTERM)
|
||||||
|
if err != nil {
|
||||||
|
Config.SysLog.Fatal("", "Error stopping process group %d: %s\n", pgid, err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
/* Wait for command to finish, get exit code */
|
||||||
|
err = cmd.Wait()
|
||||||
|
exitCode := 0
|
||||||
|
if err != nil {
|
||||||
|
/* Error, try to get exit code */
|
||||||
|
exitError, ok := err.(*exec.ExitError)
|
||||||
|
if ok {
|
||||||
|
waitStatus := exitError.Sys().(syscall.WaitStatus)
|
||||||
|
exitCode = waitStatus.ExitStatus()
|
||||||
|
} else {
|
||||||
|
exitCode = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* No error! Get exit code direct from command */
|
||||||
|
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
|
||||||
|
exitCode = waitStatus.ExitStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
/* If non-zero exit code return error */
|
||||||
|
Config.SysLog.Error("", "Error executing: %s\n", path)
|
||||||
|
return &GophorError{ CommandExitCodeErr, err }
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Just neatens creating an environment KEY=VALUE string */
|
||||||
|
func envKeyValue(key, value string) string {
|
||||||
|
return key+"="+value
|
||||||
|
}
|
@ -0,0 +1,237 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GopherUrl struct {
|
||||||
|
Path string
|
||||||
|
Parameters string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
/* Just naming some constants */
|
||||||
|
DOSLineEnd = "\r\n"
|
||||||
|
UnixLineEnd = "\n"
|
||||||
|
End = "."
|
||||||
|
Tab = "\t"
|
||||||
|
LastLine = End+DOSLineEnd
|
||||||
|
|
||||||
|
/* Gopher line formatting */
|
||||||
|
MaxUserNameLen = 70 /* RFC 1436 standard, though we use user-supplied page-width */
|
||||||
|
MaxSelectorLen = 255 /* RFC 1436 standard */
|
||||||
|
SelectorErrorStr = "/max_selector_length_reached"
|
||||||
|
GophermapRenderErrorStr = ""
|
||||||
|
GophermapReadErrorStr = "Error reading subgophermap: "
|
||||||
|
GophermapExecErrorStr = "Error executing gophermap: "
|
||||||
|
|
||||||
|
/* Default null values */
|
||||||
|
NullSelector = "-"
|
||||||
|
NullHost = "null.host"
|
||||||
|
NullPort = "0"
|
||||||
|
|
||||||
|
/* Replacement strings */
|
||||||
|
ReplaceStrHostname = "$hostname"
|
||||||
|
ReplaceStrPort = "$port"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Item type characters:
|
||||||
|
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
|
||||||
|
* and Gophernicus project. Those with ALL-CAPS descriptions in
|
||||||
|
* [square brackets] defined and used by Gophernicus, a popular
|
||||||
|
* Gopher server.
|
||||||
|
*/
|
||||||
|
type ItemType byte
|
||||||
|
const (
|
||||||
|
/* RFC 1436 Standard */
|
||||||
|
TypeFile = ItemType('0') /* Regular file (text) */
|
||||||
|
TypeDirectory = ItemType('1') /* Directory (menu) */
|
||||||
|
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
|
||||||
|
TypeError = ItemType('3') /* Error message */
|
||||||
|
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
|
||||||
|
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
|
||||||
|
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
|
||||||
|
TypeSearch = ItemType('7') /* Query search engine or CGI script */
|
||||||
|
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
|
||||||
|
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
|
||||||
|
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
|
||||||
|
TypeGif = ItemType('g') /* GIF format image file (just use I) */
|
||||||
|
TypeImage = ItemType('I') /* Any format image file */
|
||||||
|
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
|
||||||
|
|
||||||
|
/* GopherII Standard */
|
||||||
|
TypeCalendar = ItemType('c') /* Calendar file */
|
||||||
|
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
|
||||||
|
TypeHtml = ItemType('h') /* HTML document */
|
||||||
|
TypeInfo = ItemType('i') /* Informational text (not selectable) */
|
||||||
|
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
|
||||||
|
TypeMail = ItemType('M') /* Email repository (MBOX) */
|
||||||
|
TypeAudio = ItemType('s') /* Audio recordings */
|
||||||
|
TypeXml = ItemType('x') /* eXtensible Markup Language document */
|
||||||
|
TypeVideo = ItemType(';') /* Video files */
|
||||||
|
|
||||||
|
/* Commonly Used */
|
||||||
|
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
|
||||||
|
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
|
||||||
|
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
|
||||||
|
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
|
||||||
|
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
|
||||||
|
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on directory listing */
|
||||||
|
|
||||||
|
/* Default type */
|
||||||
|
TypeDefault = TypeBin
|
||||||
|
|
||||||
|
/* Gophor specific types */
|
||||||
|
TypeInfoNotStated = ItemType('I') /* [INTERNAL USE] */
|
||||||
|
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
|
||||||
|
)
|
||||||
|
|
||||||
|
var FileExtMap = map[string]ItemType{
|
||||||
|
".out": TypeBin,
|
||||||
|
".a": TypeBin,
|
||||||
|
".o": TypeBin,
|
||||||
|
".ko": TypeBin, /* ... Though tbh, kernel extensions?!!! */
|
||||||
|
".msi": TypeBin,
|
||||||
|
".exe": TypeBin,
|
||||||
|
|
||||||
|
".gophermap": TypeDirectory,
|
||||||
|
|
||||||
|
".lz": TypeBinArchive,
|
||||||
|
".gz": TypeBinArchive,
|
||||||
|
".bz2": TypeBinArchive,
|
||||||
|
".7z": TypeBinArchive,
|
||||||
|
".zip": TypeBinArchive,
|
||||||
|
|
||||||
|
".gitignore": TypeFile,
|
||||||
|
".txt": TypeFile,
|
||||||
|
".json": TypeFile,
|
||||||
|
".yaml": TypeFile,
|
||||||
|
".ocaml": TypeFile,
|
||||||
|
".s": TypeFile,
|
||||||
|
".c": TypeFile,
|
||||||
|
".py": TypeFile,
|
||||||
|
".h": TypeFile,
|
||||||
|
".go": TypeFile,
|
||||||
|
".fs": TypeFile,
|
||||||
|
".odin": TypeFile,
|
||||||
|
".nanorc": TypeFile,
|
||||||
|
".bashrc": TypeFile,
|
||||||
|
".mkshrc": TypeFile,
|
||||||
|
".vimrc": TypeFile,
|
||||||
|
".vim": TypeFile,
|
||||||
|
".viminfo": TypeFile,
|
||||||
|
".sh": TypeFile,
|
||||||
|
".conf": TypeFile,
|
||||||
|
".xinitrc": TypeFile,
|
||||||
|
".jstarrc": TypeFile,
|
||||||
|
".joerc": TypeFile,
|
||||||
|
".jpicorc": TypeFile,
|
||||||
|
".profile": TypeFile,
|
||||||
|
".bash_profile": TypeFile,
|
||||||
|
".bash_logout": TypeFile,
|
||||||
|
".log": TypeFile,
|
||||||
|
".ovpn": TypeFile,
|
||||||
|
|
||||||
|
".md": TypeMarkup,
|
||||||
|
|
||||||
|
".xml": TypeXml,
|
||||||
|
|
||||||
|
".doc": TypeDoc,
|
||||||
|
".docx": TypeDoc,
|
||||||
|
".pdf": TypeDoc,
|
||||||
|
|
||||||
|
".jpg": TypeImage,
|
||||||
|
".jpeg": TypeImage,
|
||||||
|
".png": TypeImage,
|
||||||
|
".gif": TypeImage,
|
||||||
|
|
||||||
|
".html": TypeHtml,
|
||||||
|
".htm": TypeHtml,
|
||||||
|
|
||||||
|
".ogg": TypeAudio,
|
||||||
|
".mp3": TypeAudio,
|
||||||
|
".wav": TypeAudio,
|
||||||
|
".mod": TypeAudio,
|
||||||
|
".it": TypeAudio,
|
||||||
|
".xm": TypeAudio,
|
||||||
|
".mid": TypeAudio,
|
||||||
|
".vgm": TypeAudio,
|
||||||
|
".opus": TypeAudio,
|
||||||
|
".m4a": TypeAudio,
|
||||||
|
".aac": TypeAudio,
|
||||||
|
|
||||||
|
".mp4": TypeVideo,
|
||||||
|
".mkv": TypeVideo,
|
||||||
|
".webm": TypeVideo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build error line */
|
||||||
|
func buildErrorLine(selector string) []byte {
|
||||||
|
ret := string(TypeError)
|
||||||
|
ret += selector + DOSLineEnd
|
||||||
|
ret += LastLine
|
||||||
|
return []byte(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build gopher compliant line with supplied information */
|
||||||
|
func buildLine(t ItemType, name, selector, host string, port string) []byte {
|
||||||
|
ret := string(t)
|
||||||
|
|
||||||
|
/* Add name, truncate name if too long */
|
||||||
|
if len(name) > Config.PageWidth {
|
||||||
|
ret += name[:Config.PageWidth-5]+"..."+Tab
|
||||||
|
} else {
|
||||||
|
ret += name+Tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add selector. If too long use err, skip if empty */
|
||||||
|
selectorLen := len(selector)
|
||||||
|
if selectorLen > MaxSelectorLen {
|
||||||
|
ret += SelectorErrorStr+Tab
|
||||||
|
} else if selectorLen > 0 {
|
||||||
|
ret += selector+Tab
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add host + port */
|
||||||
|
ret += host+Tab+port+DOSLineEnd
|
||||||
|
|
||||||
|
return []byte(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build gopher compliant info line */
|
||||||
|
func buildInfoLine(content string) []byte {
|
||||||
|
return buildLine(TypeInfo, content, NullSelector, NullHost, NullPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get item type for named file on disk */
|
||||||
|
func getItemType(name string) ItemType {
|
||||||
|
/* Split, name MUST be lower */
|
||||||
|
split := strings.Split(strings.ToLower(name), ".")
|
||||||
|
|
||||||
|
/* First we look at how many '.' in name string */
|
||||||
|
splitLen := len(split)
|
||||||
|
switch splitLen {
|
||||||
|
case 0:
|
||||||
|
/* Always return TypeDefault. We can never tell */
|
||||||
|
return TypeDefault
|
||||||
|
|
||||||
|
default:
|
||||||
|
/* Get index of str after last ".", look in FileExtMap */
|
||||||
|
fileType, ok := FileExtMap["."+split[splitLen-1]]
|
||||||
|
if ok {
|
||||||
|
return fileType
|
||||||
|
} else {
|
||||||
|
return TypeDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Build a line separator of supplied width */
|
||||||
|
func buildLineSeparator(count int) string {
|
||||||
|
ret := ""
|
||||||
|
for i := 0; i < count; i += 1 {
|
||||||
|
ret += "_"
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpStripWriter struct {
|
||||||
|
/* Wrapper to io.Writer that reads a predetermined amount into a buffer
|
||||||
|
* then parses the buffer for valid HTTP headers and status code, deciding
|
||||||
|
* whether to strip these headers or returning with an HTTP status code.
|
||||||
|
*/
|
||||||
|
Writer io.Writer
|
||||||
|
SkipBuffer []byte
|
||||||
|
SkipIndex int
|
||||||
|
Err *GophorError
|
||||||
|
|
||||||
|
/* We set underlying write function with a variable, so that each call
|
||||||
|
* to .Write() doesn't have to perform a check every time whether we need
|
||||||
|
* to keep checking for headers to skip.
|
||||||
|
*/
|
||||||
|
WriteFunc func([]byte) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpStripWriter(writer io.Writer) *HttpStripWriter {
|
||||||
|
w := &HttpStripWriter{}
|
||||||
|
w.Writer = writer
|
||||||
|
w.WriteFunc = w.WriteCheckForHeaders
|
||||||
|
w.SkipBuffer = make([]byte, Config.SkipPrefixBufSize)
|
||||||
|
w.SkipIndex = 0
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) Size() int {
|
||||||
|
/* Size of the skip buffer */
|
||||||
|
return len(w.SkipBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) Available() int {
|
||||||
|
/* How much space have we got left in the skip buffer */
|
||||||
|
return w.Size() - w.SkipIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) AddToSkipBuffer(data []byte) int {
|
||||||
|
/* Figure out how much data we need to add */
|
||||||
|
toAdd := w.Available()
|
||||||
|
if len(data) < toAdd {
|
||||||
|
toAdd = len(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add the data to the skip buffer! */
|
||||||
|
copy(w.SkipBuffer[w.SkipIndex:], data[:toAdd])
|
||||||
|
w.SkipIndex += toAdd
|
||||||
|
return toAdd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) ParseHttpHeaderSection() (bool, bool) {
|
||||||
|
/* Check if this is a valid HTTP header section and determine from status if we should continue */
|
||||||
|
validHeaderSection, shouldContinue := false, true
|
||||||
|
for _, header := range bytes.Split(w.SkipBuffer, []byte(DOSLineEnd)) {
|
||||||
|
header = bytes.ToLower(header)
|
||||||
|
|
||||||
|
if bytes.Contains(header, []byte("content-type: ")) {
|
||||||
|
/* This whole header section is now _valid_ */
|
||||||
|
validHeaderSection = true
|
||||||
|
} else if bytes.Contains(header, []byte("status: ")) {
|
||||||
|
/* Try parse status code */
|
||||||
|
statusStr := string(bytes.Split(bytes.TrimPrefix(header, []byte("status: ")), []byte(" "))[0])
|
||||||
|
|
||||||
|
if statusStr == "200" {
|
||||||
|
/* We ignore this */
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Any other values indicate error, we should not continue writing */
|
||||||
|
shouldContinue = false
|
||||||
|
|
||||||
|
/* Try parse error code */
|
||||||
|
errorCode := CgiStatusUnknownErr
|
||||||
|
switch statusStr {
|
||||||
|
case "400":
|
||||||
|
errorCode = CgiStatus400Err
|
||||||
|
case "401":
|
||||||
|
errorCode = CgiStatus401Err
|
||||||
|
case "403":
|
||||||
|
errorCode = CgiStatus403Err
|
||||||
|
case "404":
|
||||||
|
errorCode = CgiStatus404Err
|
||||||
|
case "408":
|
||||||
|
errorCode = CgiStatus408Err
|
||||||
|
case "410":
|
||||||
|
errorCode = CgiStatus410Err
|
||||||
|
case "500":
|
||||||
|
errorCode = CgiStatus500Err
|
||||||
|
case "501":
|
||||||
|
errorCode = CgiStatus501Err
|
||||||
|
case "503":
|
||||||
|
errorCode = CgiStatus503Err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set struct error */
|
||||||
|
w.Err = &GophorError{ errorCode, nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validHeaderSection, shouldContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) WriteSkipBuffer() (bool, error) {
|
||||||
|
defer func() {
|
||||||
|
w.SkipIndex = 0
|
||||||
|
}()
|
||||||
|
|
||||||
|
/* First try parse the headers, determine what to do next */
|
||||||
|
validHeaders, shouldContinue := w.ParseHttpHeaderSection()
|
||||||
|
|
||||||
|
if validHeaders {
|
||||||
|
/* Valid headers, we don't bother writing. Return whether
|
||||||
|
* shouldContinue whatever value that may be.
|
||||||
|
*/
|
||||||
|
return shouldContinue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default is to write skip buffer contents. shouldContinue only
|
||||||
|
* means something as long as we have valid headers.
|
||||||
|
*/
|
||||||
|
_, err := w.Writer.Write(w.SkipBuffer[:w.SkipIndex])
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) FinishUp() *GophorError {
|
||||||
|
/* If SkipBuffer still has contents, in case of data written being less
|
||||||
|
* than w.Size() --> check this data for HTTP headers to strip, parse
|
||||||
|
* any status codes and write this content with underlying writer if
|
||||||
|
* necessary.
|
||||||
|
*/
|
||||||
|
if w.SkipIndex > 0 {
|
||||||
|
w.WriteSkipBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return HttpStripWriter error code if set */
|
||||||
|
return w.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) Write(data []byte) (int, error) {
|
||||||
|
/* Write using whatever write function is currently set */
|
||||||
|
return w.WriteFunc(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) WriteRegular(data []byte) (int, error) {
|
||||||
|
/* Regular write function */
|
||||||
|
return w.Writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *HttpStripWriter) WriteCheckForHeaders(data []byte) (int, error) {
|
||||||
|
split := bytes.Split(data, []byte(DOSLineEnd+DOSLineEnd))
|
||||||
|
if len(split) == 1 {
|
||||||
|
/* Try add these to skip buffer */
|
||||||
|
added := w.AddToSkipBuffer(data)
|
||||||
|
|
||||||
|
if added < len(data) {
|
||||||
|
defer func() {
|
||||||
|
/* Having written skipbuffer after this if clause, set write to regular */
|
||||||
|
w.WriteFunc = w.WriteRegular
|
||||||
|
}()
|
||||||
|
|
||||||
|
doContinue, err := w.WriteSkipBuffer()
|
||||||
|
if !doContinue {
|
||||||
|
return len(data), io.EOF
|
||||||
|
} else if err != nil {
|
||||||
|
return added, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write remaining data not added to skip buffer */
|
||||||
|
count, err := w.Writer.Write(data[added:])
|
||||||
|
if err != nil {
|
||||||
|
return added+count, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(data), nil
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
/* No use for skip buffer after this clause, set write to regular */
|
||||||
|
w.WriteFunc = w.WriteRegular
|
||||||
|
w.SkipIndex = 0
|
||||||
|
}()
|
||||||
|
|
||||||
|
/* Try add what we can to skip buffer */
|
||||||
|
added := w.AddToSkipBuffer(append(split[0], []byte(DOSLineEnd+DOSLineEnd)...))
|
||||||
|
|
||||||
|
/* Write skip buffer data if necessary, check if we should continue */
|
||||||
|
doContinue, err := w.WriteSkipBuffer()
|
||||||
|
if !doContinue {
|
||||||
|
return len(data), io.EOF
|
||||||
|
} else if err != nil {
|
||||||
|
return added, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write remaining data not added to skip buffer */
|
||||||
|
count, err := w.Writer.Write(data[added:])
|
||||||
|
if err != nil {
|
||||||
|
return added+count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Parse a request string into a path and parameters string */
|
||||||
|
func parseGopherUrl(request string) (*GopherUrl, *GophorError) {
|
||||||
|
if strings.Contains(request, "#") || // we don't support fragments
|
||||||
|
strings.HasPrefix(request, "GET ") { // we don't support HTTP requests
|
||||||
|
return nil, &GophorError{ InvalidRequestErr, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if string contains any ASCII control byte */
|
||||||
|
for i := 0; i < len(request); i += 1 {
|
||||||
|
if request[i] < ' ' || request[i] == 0x7f {
|
||||||
|
return nil, &GophorError{ InvalidRequestErr, nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Split into 2 substrings by '?'. Url path and query */
|
||||||
|
split := strings.SplitN(request, "?", 2)
|
||||||
|
|
||||||
|
/* Unescape path */
|
||||||
|
path, err := url.PathUnescape(split[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, &GophorError{ InvalidRequestErr, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return GopherUrl based on this split request */
|
||||||
|
if len(split) == 1 {
|
||||||
|
return &GopherUrl{ path, "" }, nil
|
||||||
|
} else {
|
||||||
|
return &GopherUrl{ path, split[1] }, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parse line type from contents */
|
||||||
|
func parseLineType(line string) ItemType {
|
||||||
|
lineLen := len(line)
|
||||||
|
|
||||||
|
if lineLen == 0 {
|
||||||
|
return TypeInfoNotStated
|
||||||
|
} else if lineLen == 1 {
|
||||||
|
/* The only accepted types for a length 1 line */
|
||||||
|
switch ItemType(line[0]) {
|
||||||
|
case TypeEnd:
|
||||||
|
return TypeEnd
|
||||||
|
case TypeEndBeginList:
|
||||||
|
return TypeEndBeginList
|
||||||
|
case TypeComment:
|
||||||
|
return TypeComment
|
||||||
|
case TypeInfo:
|
||||||
|
return TypeInfo
|
||||||
|
case TypeTitle:
|
||||||
|
return TypeTitle
|
||||||
|
default:
|
||||||
|
return TypeUnknown
|
||||||
|
}
|
||||||
|
} else if !strings.Contains(line, string(Tab)) {
|
||||||
|
/* The only accepted types for a line with no tabs */
|
||||||
|
switch ItemType(line[0]) {
|
||||||
|
case TypeComment:
|
||||||
|
return TypeComment
|
||||||
|
case TypeTitle:
|
||||||
|
return TypeTitle
|
||||||
|
case TypeInfo:
|
||||||
|
return TypeInfo
|
||||||
|
case TypeHiddenFile:
|
||||||
|
return TypeHiddenFile
|
||||||
|
case TypeSubGophermap:
|
||||||
|
return TypeSubGophermap
|
||||||
|
default:
|
||||||
|
return TypeInfoNotStated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ItemType(line[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parses a line in a gophermap into a new request object */
|
||||||
|
func parseLineRequestString(requestPath *RequestPath, lineStr string) (*Request, *GophorError) {
|
||||||
|
if strings.HasPrefix(lineStr, "/") {
|
||||||
|
/* Assume is absolute (well, seeing server root as '/') */
|
||||||
|
if withinCgiBin(lineStr[1:]) {
|
||||||
|
/* CGI script, parse request path and parameters */
|
||||||
|
url, gophorErr := parseGopherUrl(lineStr[1:])
|
||||||
|
if gophorErr != nil {
|
||||||
|
return nil, gophorErr
|
||||||
|
} else {
|
||||||
|
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Regular file, no more parsing */
|
||||||
|
return &Request{ NewRequestPath(requestPath.RootDir(), lineStr[1:]), "" }, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Assume relative to current directory */
|
||||||
|
if withinCgiBin(lineStr) && requestPath.Relative() == "" {
|
||||||
|
/* If begins with cgi-bin and is at root dir, parse as cgi-bin */
|
||||||
|
url, gophorErr := parseGopherUrl(lineStr)
|
||||||
|
if gophorErr != nil {
|
||||||
|
return nil, gophorErr
|
||||||
|
} else {
|
||||||
|
return &Request{ NewRequestPath(requestPath.RootDir(), url.Path), url.Parameters }, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Regular file, no more parsing */
|
||||||
|
return &Request{ NewRequestPath(requestPath.RootDir(), requestPath.JoinCurDir(lineStr)), "" }, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Split a string according to a rune, that supports delimiting with '\' */
|
||||||
|
func splitStringByRune(str string, r rune) []string {
|
||||||
|
ret := make([]string, 0)
|
||||||
|
buf := ""
|
||||||
|
delim := false
|
||||||
|
for _, c := range str {
|
||||||
|
switch c {
|
||||||
|
case r:
|
||||||
|
if !delim {
|
||||||
|
ret = append(ret, buf)
|
||||||
|
buf = ""
|
||||||
|
} else {
|
||||||
|
buf += string(c)
|
||||||
|
delim = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
if !delim {
|
||||||
|
delim = true
|
||||||
|
} else {
|
||||||
|
buf += string(c)
|
||||||
|
delim = false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if !delim {
|
||||||
|
buf += string(c)
|
||||||
|
} else {
|
||||||
|
buf += "\\"+string(c)
|
||||||
|
delim = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) > 0 || len(ret) == 0 {
|
||||||
|
ret = append(ret, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestPath struct {
|
||||||
|
/* Path structure to allow hosts at
|
||||||
|
* different roots while maintaining relative
|
||||||
|
* and absolute path names for filesystem reading
|
||||||
|
*/
|
||||||
|
|
||||||
|
Root string
|
||||||
|
Rel string
|
||||||
|
Abs string
|
||||||
|
Select string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequestPath(rootDir, relPath string) *RequestPath {
|
||||||
|
return &RequestPath{ rootDir, relPath, path.Join(rootDir, strings.TrimSuffix(relPath, "/")), relPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) RemapPath(newPath string) *RequestPath {
|
||||||
|
requestPath := NewRequestPath(rp.RootDir(), sanitizeRawPath(rp.RootDir(), newPath))
|
||||||
|
requestPath.Select = rp.Relative()
|
||||||
|
return requestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) RootDir() string {
|
||||||
|
return rp.Root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) Relative() string {
|
||||||
|
return rp.Rel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) Absolute() string {
|
||||||
|
return rp.Abs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) Selector() string {
|
||||||
|
if rp.Select == "." {
|
||||||
|
return "/"
|
||||||
|
} else {
|
||||||
|
return "/"+rp.Select
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) JoinRel(extPath string) string {
|
||||||
|
return path.Join(rp.Relative(), extPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) JoinAbs(extPath string) string {
|
||||||
|
return path.Join(rp.Absolute(), extPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) JoinSelector(extPath string) string {
|
||||||
|
return path.Join(rp.Selector(), extPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) HasAbsPrefix(prefix string) bool {
|
||||||
|
return strings.HasPrefix(rp.Absolute(), prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) HasRelPrefix(prefix string) bool {
|
||||||
|
return strings.HasPrefix(rp.Relative(), prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) HasRelSuffix(suffix string) bool {
|
||||||
|
return strings.HasSuffix(rp.Relative(), suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) HasAbsSuffix(suffix string) bool {
|
||||||
|
return strings.HasSuffix(rp.Absolute(), suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) TrimRelSuffix(suffix string) string {
|
||||||
|
return strings.TrimSuffix(strings.TrimSuffix(rp.Relative(), suffix), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) TrimAbsSuffix(suffix string) string {
|
||||||
|
return strings.TrimSuffix(strings.TrimSuffix(rp.Absolute(), suffix), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) JoinCurDir(extPath string) string {
|
||||||
|
return path.Join(path.Dir(rp.Relative()), extPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rp *RequestPath) JoinRootDir(extPath string) string {
|
||||||
|
return path.Join(rp.RootDir(), extPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
/* Holds onto a request path to the filesystem and
|
||||||
|
* a string slice of parsed parameters (usually nil
|
||||||
|
* or length 1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
Path *RequestPath
|
||||||
|
Parameters string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSanitizedRequest(rootDir string, url *GopherUrl) *Request {
|
||||||
|
return &Request{
|
||||||
|
NewRequestPath(
|
||||||
|
rootDir,
|
||||||
|
sanitizeRawPath(rootDir, url.Path),
|
||||||
|
),
|
||||||
|
url.Parameters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sanitize a request path string */
|
||||||
|
func sanitizeRawPath(rootDir, relPath string) string {
|
||||||
|
/* Start with a clean :) */
|
||||||
|
relPath = path.Clean(relPath)
|
||||||
|
|
||||||
|
if path.IsAbs(relPath) {
|
||||||
|
/* Is absolute. Try trimming root and leading '/' */
|
||||||
|
relPath = strings.TrimPrefix(strings.TrimPrefix(relPath, rootDir), "/")
|
||||||
|
} else {
|
||||||
|
/* Is relative. If back dir traversal, give them root */
|
||||||
|
if strings.HasPrefix(relPath, "..") {
|
||||||
|
relPath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relPath
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Responder struct {
|
||||||
|
Conn *BufferedDeadlineConn
|
||||||
|
Host *ConnHost
|
||||||
|
Client *ConnClient
|
||||||
|
Request *Request
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponder(conn *BufferedDeadlineConn, host *ConnHost, client *ConnClient, request *Request) *Responder {
|
||||||
|
return &Responder{ conn, host, client, request }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) AccessLogInfo(format string, args ...interface{}) {
|
||||||
|
Config.AccLog.Info("("+r.Client.Ip()+") ", format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) AccessLogError(format string, args ...interface{}) {
|
||||||
|
Config.AccLog.Error("("+r.Client.Ip()+") ", format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) Write(b []byte) (int, error) {
|
||||||
|
return r.Conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) WriteData(data []byte) *GophorError {
|
||||||
|
err := r.Conn.WriteData(data)
|
||||||
|
if err != nil {
|
||||||
|
return &GophorError{ SocketWriteErr, err }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) WriteRaw(reader io.Reader) *GophorError {
|
||||||
|
err := r.Conn.WriteRaw(reader)
|
||||||
|
if err != nil {
|
||||||
|
return &GophorError{ SocketWriteRawErr, err }
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Responder) CloneWithRequest(request *Request) *Responder {
|
||||||
|
/* Create new copy of Responder only with request differring */
|
||||||
|
return &Responder{
|
||||||
|
r.Conn,
|
||||||
|
r.Host,
|
||||||
|
r.Client,
|
||||||
|
request,
|
||||||
|
}
|
||||||
|
}
|
@ -1,155 +1,71 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Worker struct {
|
type Worker struct {
|
||||||
Conn *GophorConn
|
Conn *BufferedDeadlineConn
|
||||||
}
|
Host *ConnHost
|
||||||
|
Client *ConnClient
|
||||||
func NewWorker(conn *GophorConn) *Worker {
|
RootDir string
|
||||||
return &Worker{ conn }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (worker *Worker) Serve() {
|
func (worker *Worker) Serve() {
|
||||||
defer func() {
|
defer worker.Conn.Close()
|
||||||
/* Close-up shop */
|
|
||||||
worker.Conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var count int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
/* Read buffer + final result */
|
|
||||||
buf := make([]byte, SocketReadBufSize)
|
|
||||||
received := make([]byte, 0)
|
|
||||||
|
|
||||||
iter := 0
|
|
||||||
for {
|
|
||||||
/* Buffered read from listener */
|
|
||||||
count, err = worker.Conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
Config.LogSystemError("Error reading from socket on port %s: %s\n", worker.Conn.Host.Port, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only copy non-null bytes */
|
|
||||||
received = append(received, buf[:count]...)
|
|
||||||
|
|
||||||
/* If count is less than expected read size, we've hit EOF */
|
|
||||||
if count < SocketReadBufSize {
|
|
||||||
/* EOF */
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hit max read chunk size, send error + close connection */
|
|
||||||
if iter == MaxSocketReadChunks {
|
|
||||||
Config.LogSystemError("Reached max socket read size %d. Closing connection...\n", MaxSocketReadChunks*SocketReadBufSize)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keep count :) */
|
|
||||||
iter += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle request */
|
|
||||||
gophorErr := worker.RespondGopher(received)
|
|
||||||
|
|
||||||
/* Handle any error */
|
|
||||||
if gophorErr != nil {
|
|
||||||
Config.LogSystemError("%s\n", gophorErr.Error())
|
|
||||||
|
|
||||||
/* Generate response bytes from error code */
|
line, err := worker.Conn.ReadLine()
|
||||||
response := generateGopherErrorResponseFromCode(gophorErr.Code)
|
|
||||||
|
|
||||||
/* If we got response bytes to send? SEND 'EM! */
|
|
||||||
if response != nil {
|
|
||||||
/* No gods. No masters. We don't care about error checking here */
|
|
||||||
worker.SendRaw(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (worker *Worker) SendRaw(b []byte) *GophorError {
|
|
||||||
count, err := worker.Conn.Write(b)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &GophorError{ SocketWriteErr, err }
|
Config.SysLog.Error("", "Error reading from socket port %s: %s\n", worker.Host.Port(), err.Error())
|
||||||
} else if count != len(b) {
|
return
|
||||||
return &GophorError{ SocketWriteCountErr, nil }
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (worker *Worker) Log(format string, args ...interface{}) {
|
|
||||||
Config.LogAccess(worker.Conn.RemoteAddr().String(), format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (worker *Worker) LogError(format string, args ...interface{}) {
|
/* Drop up to first tab */
|
||||||
Config.LogAccessError(worker.Conn.RemoteAddr().String(), format, args...)
|
received := strings.Split(string(line), Tab)[0]
|
||||||
}
|
|
||||||
|
|
||||||
func (worker *Worker) RespondGopher(data []byte) *GophorError {
|
|
||||||
/* According to Gopher spec, only read up to first Tab or Crlf */
|
|
||||||
dataStr := readUpToFirstTabOrCrlf(data)
|
|
||||||
|
|
||||||
/* Handle URL request if presented */
|
/* Handle URL request if presented */
|
||||||
lenBefore := len(dataStr)
|
lenBefore := len(received)
|
||||||
dataStr = strings.TrimPrefix(dataStr, "URL:")
|
received = strings.TrimPrefix(received, "URL:")
|
||||||
switch len(dataStr) {
|
switch len(received) {
|
||||||
case lenBefore-4:
|
case lenBefore-4:
|
||||||
/* Send an HTML redirect to supplied URL */
|
/* Send an HTML redirect to supplied URL */
|
||||||
worker.Log("Redirecting to %s\n", dataStr)
|
Config.AccLog.Info("("+worker.Client.Ip()+") ", "Redirecting to %s\n", received)
|
||||||
return worker.SendRaw(generateHtmlRedirect(dataStr))
|
worker.Conn.Write(generateHtmlRedirect(received))
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
/* Do nothing */
|
/* Do nothing */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sanitize supplied path */
|
/* Create GopherUrl object from request string */
|
||||||
requestPath := sanitizePath(dataStr)
|
url, gophorErr := parseGopherUrl(received)
|
||||||
|
if gophorErr == nil {
|
||||||
/* Append lastline */
|
/* Create new request from url object */
|
||||||
response, gophorErr := Config.FileSystem.HandleRequest(requestPath, worker.Conn.Host)
|
request := NewSanitizedRequest(worker.RootDir, url)
|
||||||
if gophorErr != nil {
|
|
||||||
worker.LogError("Failed to serve: %s\n", requestPath)
|
|
||||||
return gophorErr
|
|
||||||
}
|
|
||||||
worker.Log("Served: %s\n", requestPath)
|
|
||||||
|
|
||||||
/* Serve response */
|
/* Create new responder from request */
|
||||||
return worker.SendRaw(response)
|
responder := NewResponder(worker.Conn, worker.Host, worker.Client, request)
|
||||||
}
|
|
||||||
|
|
||||||
func readUpToFirstTabOrCrlf(data []byte) string {
|
/* Handle request with supplied responder */
|
||||||
/* Only read up to first tab or cr-lf */
|
gophorErr = Config.FileSystem.HandleRequest(responder)
|
||||||
dataStr := ""
|
if gophorErr == nil {
|
||||||
dataLen := len(data)
|
/* Log success to access and return! */
|
||||||
for i := 0; i < dataLen; i += 1 {
|
responder.AccessLogInfo("Served: %s\n", request.Path.Absolute())
|
||||||
switch data[i] {
|
return
|
||||||
case '\t':
|
} else {
|
||||||
return dataStr
|
/* Log failure to access */
|
||||||
case DOSLineEnd[0]:
|
responder.AccessLogError("Failed to serve: %s\n", request.Path.Absolute())
|
||||||
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
|
|
||||||
return dataStr
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
dataStr += string(data[i])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataStr
|
/* Log serve failure to error to system */
|
||||||
}
|
Config.SysLog.Error("", gophorErr.Error())
|
||||||
|
|
||||||
func sanitizePath(dataStr string) string {
|
/* Generate response bytes from error code */
|
||||||
/* Clean path and trim '/' prefix if still exists */
|
errResponse := generateGopherErrorResponseFromCode(gophorErr.Code)
|
||||||
requestPath := strings.TrimPrefix(path.Clean(dataStr), "/")
|
|
||||||
|
|
||||||
if requestPath == "." {
|
/* If we got response bytes to send? SEND 'EM! */
|
||||||
requestPath = "/"
|
if errResponse != nil {
|
||||||
} else if !strings.HasPrefix(requestPath, "/") {
|
/* No gods. No masters. We don't care about error checking here */
|
||||||
requestPath = "/" + requestPath
|
worker.Conn.WriteData(errResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestPath
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue