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