Merge pull request #4 from grufwub/development

Latest development pull
This commit is contained in:
Kim 2020-04-20 22:12:37 +01:00 committed by GitHub
commit 158e79aea5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 863 additions and 290 deletions

1
.gitignore vendored
View File

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

210
README.md
View File

@ -13,22 +13,29 @@ I'm unemployed (not due to lack of effort...) and work on open-source projects
like this and many others for free. If you would like to help support my work
that would be hugely appreciated 💕 https://liberapay.com/grufwub/
WARNING: the development branch is filled with lava, fear and capitalism.
# Usage
```
gophor [args]
-root Change server root directory.
-port Change server listening port.
-port Change server NON-TLS listening port.
-hostname Change server hostname (FQDN, used to craft dir lists).
-bind-addr Change server bind-address (used in creating socket).
-uid Change UID to drop privileges to.
-gid Change GID to drop privileges to.
-system-log Path to gophor system log file, else use stderr.
-access-log Path to gophor access log file, else use stderr.
-cache-check Change file-cache freshness check frequency (in secs).
-cache-check Change file-cache freshness check frequency.
-cache-size Change max no. files in file-cache.
-cache-file-max Change maximum allowed size of a cached file.
-page-width Change page width used when formatting output.
-restrict-files New-line separated list of regex statements restricting
files from showing in directory listing.
-description Change server description in auto generated caps.txt.
-admin-email Change admin email in auto generated caps.txt.
-geloc Change geolocation in auto generated caps.txt.
```
# Features
@ -58,71 +65,169 @@ gophor [args]
# Supported gophermap item types
All of the following item types are supported by Gophor, separated into
grouped standards. Most handling of item types is performed by the clients
connecting to Gophor, but when performing directory listings Gophor will
attempt to automatically classify files according to the below types.
Item types listed as `[SERVER ONLY]` means that these are item types
recognised ONLY by Gophor and to be used when crafting a gophermap. They
provide additional methods of formatting / functionality within a gophermap,
and the output of these item types is usually converted to informational
text lines before sending to connecting clients.
```
0 -- regular file (text)
1 -- directory (menu)
2 -- CSO phone-book server... should you be using this in 2020 lmao
3 -- Error
4 -- Binhexed macintosh file
5 -- DOS bin archive
6 -- Unix uuencoded file
7 -- Index-search server
8 -- Text-based telnet session
9 -- Binary file
T -- Text-based tn3270 session... in 2020???
g -- Gif format graphic
I -- Image file of some kind
RFC 1436 Standard:
Type | Treat as | Meaning
0 | TEXT | Regular file (text)
1 | MENU | Directory (menu)
2 | EXTERNAL | CCSO flat db; other db
3 | ERROR | Error message
4 | TEXT | Macintosh BinHex file
5 | BINARY | Binary archive (zip, rar, 7zip, tar, gzip, etc)
6 | TEXT | UUEncoded archive
7 | INDEX | Query search engine or CGI script
8 | EXTERNAL | Telnet to: VT100 series server
9 | BINARY | Binary file (see also, 5)
T | EXTERNAL | Telnet to: tn3270 series server
g | BINARY | GIF format image file (just use I)
I | BINARY | Any format image file
+ | - | Redundant (indicates mirror of previous item)
+ -- Redundant server
GopherII Standard:
Type | Treat as | Meaning
c | BINARY | Calendar file
d | BINARY | Word-processing document; PDF document
h | TEXT | HTML document
i | - | Informational text (not selectable)
p | TEXT | Page layout or markup document (plain text w/ ASCII tags)
m | BINARY | Email repository (MBOX)
s | BINARY | Audio recordings
x | TEXT | eXtensible Markup Language document
; | BINARY | Video files
. -- Lastline if this followed by CrLf
Commonly used:
Type | Treat as | Meaning
! | - | [SERVER ONLY] Menu title (set title ONCE per gophermap)
# | - | [SERVER ONLY] Comment, rest of line is ignored
- | - | [SERVER ONLY] Hide file/directory from directory listing
. | - | [SERVER ONLY] Last line -- stop processing gophermap default
* | - | [SERVER ONLY] Last line + directory listing -- stop processing
| | gophermap and end on a directory listing
= | - | [SERVER ONLY] Include subgophermap / regular file here. Prints
| | and formats file / gophermap in-place
i -- Info message
h -- HTML document
s -- Audio file
p -- PNG image
d -- Document
M -- MIME type file
; -- Video file
c -- Calendar file
! -- Title
# -- Comment (not displayed)
- -- Hide file from directory listing
= -- Include subgophermap (prints file output here)
* -- Act as-if lastline and print directory listing below
Unavailable for now due to issues with accessing path within chroot:
$ -- Execute shell command and print stdout here
Planned to be supported:
Type | Treat as | Meaning
$ | - | [SERVER ONLY] Execute shell command and print stdout here
```
# Compliance
## Item types
Supported item types are listed above.
Informational lines are sent as `i<text here>\t/\tnull.host\t0`.
Titles are sent as `i<title text>\tTITLE\tnull.host\t0`.
Web address links are sent as `h<text here>\tURL:<address>\thostname\tport`.
An HTML redirect is sent in response to any requests beginning with `URL:`.
## Policy files
Upon request, `caps.txt` can be provided from the server root directory
containing server capabiities. This can either be user or server generated.
Upon request, `robots.txt` can be provided from the server root directory
containing robot access restriction policies. This can either be user or
server generated.
## Errors
Errors are sent according to GopherII standards, terminating with a last
line if required:
`3<error text>CR-LF`
Possible Gophor errors:
```
Text | Meaning
400 Bad Request | Request not understood by server due to malformed
| syntax
401 Unauthorised | Request requires authentication
403 Forbidden | Request received but not fulfilled
404 Not Found | Server could not find anything matching requested
| URL
408 Request Time-out | Client did not produce request within server wait
| time
410 Gone | Requested resource no longer available with no
| forwarding address
500 Internal Server Error | Server encountered an unexpected condition which
| prevented request being fulfilled
501 Not Implemented | Server does not support the functionality
| required to fulfil the request
503 Service Unavailable | Server currently unable to handle the request
| due to temporary overload / maintenance
```
## Terminating full stop
Gophor will send a terminating full-stop for menus, but not for served
files.
## Placeholder text
Selector: `-`
Host: `null.host`
Port: `0`
# Todos
- TLS support
Shortterm:
- Connection throttling + timeouts
- Clean up configuration setting -- just need to rethink flags used, storing
of variables and possibly move to file-based configuration.
- Header + footer text
- Rotating logs -- have a check on start for a file-size, rotate out if the
file is too large. Possibly checks during run-time too?
- Rotating logs
- Set default charset -- need to think about implementation here...
- Set default charset
- Finish inline shell scripting support -- current thinking is to either
perform a C fork very early on, or create a separate modules binary, and
either way the 2 processes interact via some IPC method. Could allow for
other modules too.
- Autogenerated caps.txt
- Toggleable server status page (?)
- Proxy over HTTP support
- Finish inline shell scripting support
- Allow setting UID+GID via username string
- Allow setting UID+GID via username string -- not hard to implemenent, just
a lot of code and didn't want to make things to heavy too early on.
- Fix file cache only updating if main gophermap changes (but not sub files)
-- need to either rethink how we keep track of files, or rethink how
gophermaps are stored in memory.
- More fine-tuned handling of OS signals
- Improve autogenerated policy file sending -- need to rethink the worker +
response logic.
- Add support for banned file extensions (i.e. not shown in dir listing)
Longterm:
- TLS support -- requires a rethink of how we're passing port functions
generating gopher directory entries, also there is no definitive standard
for this yet
- Connection throttling + timeouts -- thread to keep track of list of
recently connected IPs. Keep incremementing connection count and only
remove from list when `lastIncremented` time is greater than timeout
- Header + footer text -- read in file / input string and format, hold in
memory than append to end of gophermaps / dir listings
- More closely follow GoLang built-in net/http code style for worker -- just
a neatness thing, maybe bring some performance improvements too and a
generally different way of approaching some of the solutions to problems we
have
# Please note
@ -134,11 +239,14 @@ As soon as we reach a stable point in development, or if other people start
contributing issues or PRs, whichever comes first, this will be changed
right away.
# Standards followed
# Resources used
Gopher-II (The Next Generation Gopher WWIS):
https://tools.ietf.org/html/draft-matavka-gopher-ii-00
Gophernicus supported item types:
https://github.com/gophernicus/gophernicus/blob/master/README.gophermap
All of the below can be viewed from your standard web browser using
floodgap's Gopher proxy:
https://gopher.floodgap.com/gopher/gw

80
build-all.sh Executable file
View File

@ -0,0 +1,80 @@
#!/bin/sh
PROJECT='gophor'
OUTDIR='build'
echo "PLEASE BE WARNED THIS SCRIPT IS WRITTEN FOR MY VOID LINUX BUILD ENVIRONMENT"
echo "YOUR CC CROSS-COMPILER LOCATIONS MAY DIFFER ON YOUR BUILD SYSTEM"
echo ""
# Clean and recreate directory
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
# Build time :)
echo "Building for linux 386..."
CGO_ENABLED=1 CC='i686-linux-musl-gcc' GOOS='linux' GOARCH='386' go build -trimpath -o "$OUTDIR/$PROJECT.linux.386" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.386"
echo ""
echo "Building for linux amd64..."
CGO_ENABLED=1 CC='x86_64-linux-musl-gcc' GOOS='linux' GOARCH='amd64' go build -trimpath -o "$OUTDIR/$PROJECT.linux.amd64" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.amd64"
echo ""
echo "Building for linux armv5..."
CGO_ENABLED=1 CC='arm-linux-musleabi-gcc' GOOS='linux' GOARCH='arm' GOARM=5 go build -trimpath -o "$OUTDIR/$PROJECT.linux.armv5" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.armv5"
echo ""
echo "Building for linux armv5hf..."
CGO_ENABLED=1 CC='arm-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=5 go build -trimpath -o "$OUTDIR/$PROJECT.linux.armv5hf" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.armv5hf"
echo ""
echo "Building for linux armv6..."
CGO_ENABLED=1 CC='arm-linux-musleabi-gcc' GOOS='linux' GOARCH='arm' GOARM=6 go build -trimpath -o "$OUTDIR/$PROJECT.linux.armv6" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.armv6"
echo ""
echo "Building for linux armv6hf..."
CGO_ENABLED=1 CC='arm-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=6 go build -trimpath -o "$OUTDIR/$PROJECT.linux.armv6hf" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.armv6hf"
echo ""
echo "Building for linux armv7hf..."
CGO_ENABLED=1 CC='armv7l-linux-musleabihf-gcc' GOOS='linux' GOARCH='arm' GOARM=7 go build -trimpath -o "$OUTDIR/$PROJECT.linux.armv7hf" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.armv7hf"
echo ""
echo "Building for linux arm64..."
CGO_ENABLED=1 CC='aarch64-linux-musl-gcc' GOOS='linux' GOARCH='arm64' go build -trimpath -o "$OUTDIR/$PROJECT.linux.arm64" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.arm64"
echo ""
echo "Building for linux mips..."
CGO_ENABLED=1 CC='mips-linux-musl-gcc' GOOS='linux' GOARCH='mips' go build -trimpath -o "$OUTDIR/$PROJECT.linux.mips" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.mips"
echo ""
echo "Building for linux mipshf..."
CGO_ENABLED=1 CC='mips-linux-muslhf-gcc' GOOS='linux' GOARCH='mips' go build -trimpath -o "$OUTDIR/$PROJECT.linux.mipshf" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.mipshf"
echo ""
echo "Building for linux mipsle..."
CGO_ENABLED=1 CC='mipsel-linux-musl-gcc' GOOS='linux' GOARCH='mipsle' go build -trimpath -o "$OUTDIR/$PROJECT.linux.mipsle" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.mipsle"
echo ""
echo "Building for linux mipslehf..."
CGO_ENABLED=1 CC='mipsel-linux-muslhf-gcc' GOOS='linux' GOARCH='mipsle' go build -trimpath -o "$OUTDIR/$PROJECT.linux.mipslehf" -buildmode 'default' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.mipslehf"
echo ""
echo "Building for linux ppc64le..."
CGO_ENABLED=1 CC='powerpc64le-linux-musl-gcc' GOOS='linux' GOARCH='ppc64le' go build -trimpath -o "$OUTDIR/$PROJECT.linux.ppc64le" -buildmode 'pie' -a -tags 'netgo' -ldflags '-s -w -extldflags "-static"'
upx -9 "$OUTDIR/$PROJECT.linux.ppc64le"
echo ""
echo "PLEASE DON'T JUDGE THIS SCRIPT, IT IS TRULY SO AWFUL. TO BE IMPROVED..."

View File

@ -1,3 +0,0 @@
#!/bin/sh
go build -trimpath -buildmode=pie

120
cache.go
View File

@ -4,12 +4,9 @@ import (
"os"
"sync"
"time"
"container/list"
)
var (
FileMonitorSleepTime = time.Duration(*CacheCheckFreq) * time.Second
/* Global file caches */
GlobalFileCache *FileCache
)
@ -19,18 +16,24 @@ func startFileCaching() {
GlobalFileCache = new(FileCache)
GlobalFileCache.Init(*CacheSize)
/* Parse the supplied CacheCheckFreq */
sleepTime, err := time.ParseDuration(*CacheCheckFreq)
if err != nil {
logSystemFatal("Error parsing supplied cache check frequency %s: %s\n", *CacheCheckFreq, err)
}
/* Start file monitor in separate goroutine */
go startFileMonitor()
go startFileMonitor(sleepTime)
}
func startFileMonitor() {
func startFileMonitor(sleepTime time.Duration) {
go func() {
for {
/* Sleep so we don't take up all the precious CPU time :) */
time.Sleep(FileMonitorSleepTime)
time.Sleep(sleepTime)
/* Check global file cache freshness */
checkCacheFreshness(GlobalFileCache)
checkCacheFreshness()
}
/* We shouldn't have reached here */
@ -38,58 +41,43 @@ func startFileMonitor() {
}()
}
func checkCacheFreshness(cache *FileCache) {
/* Before anything, get cache read lock */
cache.CacheMutex.RLock()
func checkCacheFreshness() {
/* Before anything, get cache write lock (in case we have to delete) */
GlobalFileCache.CacheMutex.Lock()
/* Iterate through paths in cache map to query file last modified times */
for path := range cache.CacheMap {
for path := range GlobalFileCache.CacheMap.Map {
stat, err := os.Stat(path)
if err != nil {
/* Gotta be speedy, skip on error */
logSystemError("failed to stat file in cache: %s\n", path)
/* Log file as not in cache, then delete */
logSystemError("Failed to stat file in cache: %s\n", path)
GlobalFileCache.CacheMap.Remove(path)
continue
}
timeModified := stat.ModTime().UnixNano()
/* Get file pointer and immediately get write lock */
file := cache.CacheMap[path].File
file.Lock()
/* Get file pointer, no need for lock as we have write lock */
file := GlobalFileCache.CacheMap.Get(path)
/* If the file is marked as fresh, but file on disk newer, mark as unfresh */
if file.IsFresh() && file.LastRefresh() < timeModified {
file.SetUnfresh()
}
/* Done with file, we can release write lock */
file.Unlock()
}
/* Done! We can release cache read lock */
cache.CacheMutex.RUnlock()
}
type FileElement struct {
File *File
Element *list.Element
GlobalFileCache.CacheMutex.Unlock()
}
/* TODO: see if there is more efficienct setup */
type FileCache struct {
CacheMap map[string]*FileElement
CacheMap *FixedMap
CacheMutex sync.RWMutex
FileList *list.List
ListMutex sync.Mutex
Size int
}
func (fc *FileCache) Init(size int) {
fc.CacheMap = make(map[string]*FileElement)
fc.CacheMap = NewFixedMap(size)
fc.CacheMutex = sync.RWMutex{}
fc.FileList = list.New()
fc.FileList.Init()
fc.ListMutex = sync.Mutex{}
fc.Size = size
}
func (fc *FileCache) FetchRegular(path string) ([]byte, *GophorError) {
@ -111,31 +99,31 @@ func (fc *FileCache) FetchGophermap(path string) ([]byte, *GophorError) {
func (fc *FileCache) Fetch(path string, newFileContents func(string) FileContents) ([]byte, *GophorError) {
/* Get cache map read lock then check if file in cache map */
fc.CacheMutex.RLock()
fileElement, ok := fc.CacheMap[path]
file := fc.CacheMap.Get(path)
/* TODO: work on efficiency */
if ok {
if file != nil {
/* File in cache -- before doing anything get file read lock */
fileElement.File.RLock()
file.RLock()
/* Check file is marked as fresh */
if !fileElement.File.IsFresh() {
if !file.IsFresh() {
/* File not fresh! Swap file read for write-lock */
fileElement.File.RUnlock()
fileElement.File.Lock()
file.RUnlock()
file.Lock()
/* Reload file contents from disk */
gophorErr := fileElement.File.LoadContents()
gophorErr := file.LoadContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
fileElement.File.Unlock()
file.Unlock()
fc.CacheMutex.RUnlock()
return nil, gophorErr
}
/* Updated! Swap back file write for read lock */
fileElement.File.Unlock()
fileElement.File.RLock()
file.Unlock()
file.RLock()
}
} else {
/* Before we do ANYTHING, we need to check file-size on disk */
@ -150,7 +138,7 @@ func (fc *FileCache) Fetch(path string, newFileContents func(string) FileContent
contents := newFileContents(path)
/* Create new file wrapper around contents */
file := NewFile(contents)
file = NewFile(contents)
/* NOTE: file isn't in cache yet so no need to lock file write mutex
* before loading from disk
@ -171,37 +159,12 @@ func (fc *FileCache) Fetch(path string, newFileContents func(string) FileContent
return b, nil
}
/* File not in cache -- Swap cache map read for write lock.
* NOTE: Here we don't need a list mutex lock as it is impossible
* for any other goroutine to get this lock while we have a cache
* _write_ lock. Due to the way this Fetch() function is written
*/
/* File not in cache -- Swap cache map read for write lock. */
fc.CacheMutex.RUnlock()
fc.CacheMutex.Lock()
/* Place path in FileList to get back element */
element := fc.FileList.PushFront(path)
/* Create fileElement and place in map */
fileElement = &FileElement{ file, element }
fc.CacheMap[path] = fileElement
/* If we're at capacity, remove last item in list from cachemap + cache list */
if fc.FileList.Len() == fc.Size {
removeElement := fc.FileList.Back()
/* Have to perform type assertion even if we know value will always be string.
* If not, we may as well os.Exit(1) out since error is fatal
*/
removePath, ok := removeElement.Value.(string)
if !ok {
logSystemFatal("non-string found in cache list!\n")
}
/* Now delete. We don't need ListMutex lock as we have cache map write lock */
delete(fc.CacheMap, removePath)
fc.FileList.Remove(removeElement)
}
/* Put file in the FixedMap */
fc.CacheMap.Put(path, file)
/* Before unlocking cache mutex, lock file read for upcoming call to .Contents() */
file.RLock()
@ -211,16 +174,9 @@ func (fc *FileCache) Fetch(path string, newFileContents func(string) FileContent
fc.CacheMutex.RLock()
}
/* Get list lock, ready to update placement in list */
fc.ListMutex.Lock()
/* Read file contents into new variable for return, then unlock file read lock */
b := fileElement.File.Contents()
fileElement.File.RUnlock()
/* Update placement in list then unlock */
fc.FileList.MoveToFront(fileElement.Element)
fc.ListMutex.Unlock()
b := file.Contents()
file.RUnlock()
/* Finally we can unlock the cache map read lock, we are done :) */
fc.CacheMutex.RUnlock()

View File

@ -1,6 +1,9 @@
package main
const (
/* Gophor */
GophorVersion = "0.1-alpha"
/* Parsing */
DOSLineEnd = "\r\n"
UnixLineEnd = "\n"
@ -12,8 +15,9 @@ const (
MaxUserNameLen = 70 /* RFC 1436 standard */
MaxSelectorLen = 255 /* RFC 1436 standard */
NullSelector = "-"
NullHost = "null.host"
NullPort = "1"
NullPort = 0
SelectorErrorStr = "selector_length_error"
GophermapRenderErrorStr = ""
@ -22,6 +26,8 @@ const (
/* Filesystem */
GophermapFileStr = "gophermap"
CapsTxtStr = "caps.txt"
RobotsTxtStr = "robots.txt"
/* Misc */
BytesInMegaByte = 1048576.0

View File

@ -8,6 +8,7 @@ import (
* Client error data structure
*/
type ErrorCode int
type ErrorResponseCode int
const (
/* Filesystem */
PathEnumerationErr ErrorCode = iota
@ -27,6 +28,19 @@ const (
EmptyItemTypeErr ErrorCode = iota
EntityPortParseErr ErrorCode = iota
InvalidGophermapErr 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
)
type GophorError struct {
@ -76,3 +90,83 @@ func (e *GophorError) Error() string {
return fmt.Sprintf("%s", str)
}
}
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 sending, no point trying to send error */
case SocketWriteErr:
return NoResponse
case SocketWriteCountErr:
return NoResponse
case InvalidRequestErr:
return ErrorResponse400
case EmptyItemTypeErr:
return ErrorResponse500
case EntityPortParseErr:
return ErrorResponse500
case InvalidGophermapErr:
return ErrorResponse500
default:
return ErrorResponse503
}
}
func generateGopherErrorResponseFromCode(code ErrorCode) []byte {
responseCode := gophorErrorToResponseCode(code)
if responseCode == NoResponse {
return nil
}
return generateGopherErrorResponse(responseCode)
}
func generateGopherErrorResponse(code ErrorResponseCode) []byte {
b := buildError(code.String())
return append(b, []byte(LastLine)...)
}
func (e ErrorResponseCode) String() string {
switch e {
case ErrorResponse200:
return "200 OK"
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 */
logSystemFatal("Unhandled ErrorResponseCode type\n")
return ""
}
}

73
fixedmap.go Normal file
View File

@ -0,0 +1,73 @@
package main
import (
"container/list"
)
/* FixedMap:
* A fixed size map that pushes the last
* used value from the stack if size limit
* is reached and user attempts .Put()
*/
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 {
fm := new(FixedMap)
fm.Map = make(map[string]*MapElement)
fm.List = list.New()
fm.Size = size
return fm
}
func (fm *FixedMap) Get(key string) *File {
elem, ok := fm.Map[key]
if ok {
return elem.Value
} else {
return nil
}
}
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)
logSystem("Popped key: %s\n", key)
}
}
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)
}

View File

@ -6,25 +6,29 @@ import (
var (
/* Base server settings */
ServerRoot = flag.String("root", "/var/gopher", "Change server root directory.")
ServerHostname = flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
ServerPort = flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).")
ServerBindAddr = flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
// ServerTlsPort = flag.Int("tls-port", 0, "Change server TLS/SSL port (0 to disable).")
// ServerTlsCert = flag.String("cert", "", "Change server TLS/SSL cert file.")
ExecAsUid = flag.Int("uid", 1000, "Change UID to drop executable privileges to.")
ExecAsGid = flag.Int("gid", 100, "Change GID to drop executable privileges to.")
ServerRoot = flag.String("root", "/var/gopher", "Change server root directory.")
ServerHostname = flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
ServerPort = flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).")
ServerBindAddr = flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
ExecAsUid = flag.Int("uid", 1000, "Change UID to drop executable privileges to.")
ExecAsGid = flag.Int("gid", 100, "Change GID to drop executable privileges to.")
/* User supplied caps.txt information */
ServerDescription = flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in auto-generated caps.txt.")
ServerAdmin = flag.String("admin-email", "", "Change admin email in auto-generated caps.txt.")
ServerGeoloc = flag.String("geoloc", "", "Change server gelocation string in auto-generated caps.txt.")
/* Content settings */
PageWidth = flag.Int("page-width", 80, "Change page width used when formatting output.")
PageWidth = flag.Int("page-width", 80, "Change page width used when formatting output.")
RestrictedFiles = flag.String("restrict-files", "", "New-line separated list of regex statements restricting files from showing in directory listings.")
/* Logging settings */
SystemLog = flag.String("system-log", "", "Change server system log file (blank outputs to stderr).")
AccessLog = flag.String("access-log", "", "Change server access log file (blank outputs to stderr).")
LoggingType = flag.Int("log-type", 0, "Change server log file handling -- 0:default 1:disable")
SystemLog = flag.String("system-log", "", "Change server system log file (blank outputs to stderr).")
AccessLog = flag.String("access-log", "", "Change server access log file (blank outputs to stderr).")
LoggingType = flag.Int("log-type", 0, "Change server log file handling -- 0:default 1:disable")
/* Cache settings */
CacheCheckFreq = flag.Float64("cache-check", 30, "Change file cache freshness check frequency (in seconds).")
CacheSize = flag.Int("cache-size", 1000, "Change individual file cache size, measured in file count.")
CacheFileSizeMax = flag.Float64("cache-file-max", 5, "Change maximum file size to be cached (in megabytes).")
CacheCheckFreq = flag.String("cache-check", "60s", "Change file cache freshness check frequency.")
CacheSize = flag.Int("cache-size", 50, "Change file cache size, measured in file count.")
CacheFileSizeMax = flag.Float64("cache-file-max", 0.5, "Change maximum file size to be cached (in megabytes).")
)

View File

@ -3,10 +3,9 @@ package main
import (
"strconv"
"strings"
"path/filepath"
)
var FileExtensions = map[string]ItemType{
var SingleFileExtMap = map[string]ItemType{
".out": TypeBin,
".a": TypeBin,
".o": TypeBin,
@ -51,6 +50,16 @@ var FileExtensions = map[string]ItemType{
".mkv": TypeVideo,
}
var DoubleFileExtMap = map[string]ItemType{
".tar.gz": TypeBin,
}
func buildError(selector string) []byte {
ret := string(TypeError)
ret += selector + DOSLineEnd
return []byte(ret)
}
func buildLine(t ItemType, name, selector, host string, port int) []byte {
ret := string(t)
@ -69,34 +78,62 @@ func buildLine(t ItemType, name, selector, host string, port int) []byte {
ret += selector+"\t"
}
/* Add host, set to nullhost if empty */
if host == "" {
ret += NullHost+"\t"
} else {
ret += host+"\t"
}
/* Add port, set to nullport if 0 */
if port == 0 {
ret += NullPort+DOSLineEnd
} else {
ret += strconv.Itoa(port)+DOSLineEnd
}
/* Add host + port */
ret += host+"\t"+strconv.Itoa(port)+DOSLineEnd
return []byte(ret)
}
func buildInfoLine(content string) []byte {
return buildLine(TypeInfo, content, "", "", 0)
return buildLine(TypeInfo, content, NullSelector, NullHost, NullPort)
}
/* getItemType(name string) ItemType:
* Here we use an empty function pointer, and set the correct
* function to be used during the restricted files regex parsing.
* This negates need to check if RestrictedFilesRegex is nil every
* single call.
*/
func getItemType(name string) ItemType {
extension := strings.ToLower(filepath.Ext(name))
fileType, ok := FileExtensions[extension]
if !ok {
return TypeDefault
/* Name MUST be lower */
nameLower := strings.ToLower(name)
/* First we look at how many '.' in name string */
switch strings.Count(nameLower, ".") {
case 0:
/* Always return TypeDefault. We can never tell */
return TypeDefault
case 1:
/* Get index of ".", try look in SingleFileExtMap */
i := strings.IndexByte(nameLower, '.')
fileType, ok := SingleFileExtMap[nameLower[i:]]
if ok {
return fileType
} else {
return TypeDefault
}
default:
/* Get index of penultimate ".", try look in DoubleFileExtMap */
i, j := len(nameLower)-1, 0
for i >= 0 {
if nameLower[i] == '.' {
if j == 1 {
break
} else {
j += 1
}
}
i -= 1
}
fileType, ok := DoubleFileExtMap[nameLower[i:]]
if ok {
return fileType
} else {
return TypeDefault
}
}
return fileType
}
func parseLineType(line string) ItemType {
@ -142,4 +179,3 @@ func parseLineType(line string) ItemType {
return ItemType(line[0])
}

88
fs.go
View File

@ -4,10 +4,10 @@ import (
"os"
"sync"
"path"
"strings"
"bytes"
"time"
"io"
"sort"
"bufio"
)
@ -46,7 +46,7 @@ func (f *File) LoadContents() *GophorError {
return gophorErr
}
/* Update lastRefresh + set fresh */
/* Update lastRefresh, set fresh, unset deletion (not likely set) */
f.lastRefresh = time.Now().UnixNano()
f.isFresh = true
@ -190,7 +190,14 @@ func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, er
return 0, nil, nil
}
func listDir(dirPath string, hidden map[string]bool) ([]byte, *GophorError) {
/* listDir():
* Here we use an empty function pointer, and set the correct
* function to be used during the restricted files regex parsing.
* This negates need to check if RestrictedFilesRegex is nil every
* single call.
*/
var listDir func(dirPath string, hidden map[string]bool) ([]byte, *GophorError)
func _listDir(dirPath string, hidden map[string]bool) ([]byte, *GophorError) {
/* Open directory file descriptor */
fd, err := os.Open(dirPath)
if err != nil {
@ -198,24 +205,27 @@ func listDir(dirPath string, hidden map[string]bool) ([]byte, *GophorError) {
return nil, &GophorError{ FileOpenErr, err }
}
/* Open directory stream for reading */
/* Read files in directory */
files, err := fd.Readdir(-1)
if err != nil {
logSystemError("failed to enumerate dir %s: %s\n", dirPath, err.Error())
return nil, &GophorError{ DirListErr, err }
}
/* Walk through directory */
/* Sort the files by name */
sort.Sort(byName(files))
/* Create directory content slice, ready */
dirContents := make([]byte, 0)
/* First add a 'back' entry. GoLang Readdir() seems to miss this */
line := buildLine(TypeDirectory, "..", path.Join(fd.Name(), ".."), *ServerHostname, *ServerPort)
dirContents = append(dirContents, line...)
/* Iterate through files :) */
/* Walk through files :D */
for _, file := range files {
/* Skip dotfiles + gophermap file + requested hidden */
if file.Name()[0] == '.' || strings.HasSuffix(file.Name(), GophermapFileStr) {
/* If requested hidden */
if _, ok := hidden[file.Name()]; ok {
continue
}
@ -241,3 +251,65 @@ func listDir(dirPath string, hidden map[string]bool) ([]byte, *GophorError) {
return dirContents, nil
}
func _listDirRegexMatch(dirPath string, hidden map[string]bool) ([]byte, *GophorError) {
/* Open directory file descriptor */
fd, err := os.Open(dirPath)
if err != nil {
logSystemError("failed to open %s: %s\n", dirPath, err.Error())
return nil, &GophorError{ FileOpenErr, err }
}
/* Read files in directory */
files, err := fd.Readdir(-1)
if err != nil {
logSystemError("failed to enumerate dir %s: %s\n", dirPath, err.Error())
return nil, &GophorError{ DirListErr, err }
}
/* Sort the files by name */
sort.Sort(byName(files))
/* Create directory content slice, ready */
dirContents := make([]byte, 0)
/* First add a 'back' entry. GoLang Readdir() seems to miss this */
line := buildLine(TypeDirectory, "..", path.Join(fd.Name(), ".."), *ServerHostname, *ServerPort)
dirContents = append(dirContents, line...)
/* Walk through files :D */
for _, file := range files {
/* If regex match in restricted files || requested hidden */
if isRestrictedFile(file.Name()) {
continue
} else if _, ok := hidden[file.Name()]; ok {
continue
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := path.Join(fd.Name(), file.Name())
line = buildLine(TypeDirectory, file.Name(), itemPath, *ServerHostname, *ServerPort)
dirContents = append(dirContents, line...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := path.Join(fd.Name(), file.Name())
itemType := getItemType(itemPath)
line = buildLine(itemType, file.Name(), itemPath, *ServerHostname, *ServerPort)
dirContents = append(dirContents, line...)
default:
/* Ignore */
}
}
return dirContents, nil
}
/* 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] }

View File

@ -94,9 +94,16 @@ func (s *GophermapDirListing) Render() ([]byte, *GophorError) {
}
func readGophermap(path string) ([]GophermapSection, *GophorError) {
/* Create return slice. Also hidden files map in case dir listing requested */
/* Create return slice */
sections := make([]GophermapSection, 0)
/* _Create_ hidden files map now in case dir listing requested */
hidden := make(map[string]bool)
/* Keep track of whether we've already come across a title line (only 1 allowed!) */
titleAlready := false
/* Reference directory listing now in case requested */
var dirListing *GophermapDirListing
/* Perform buffered scan with our supplied splitter and iterators */
@ -111,6 +118,13 @@ func readGophermap(path string) ([]GophermapSection, *GophorError) {
/* Append TypeInfo to the beginning of line */
sections = append(sections, NewGophermapText(buildInfoLine(line)))
case TypeTitle:
/* Reformat title line to send as info title */
if !titleAlready {
sections = append(sections, NewGophermapText(buildLine(TypeInfo, line[1:], "TITLE", NullHost, NullPort)))
titleAlready = true
}
case TypeComment:
/* We ignore this line */
break

View File

@ -2,8 +2,8 @@ package main
import (
"log"
"fmt"
"os"
"strconv"
"syscall"
"os/signal"
"flag"
@ -28,9 +28,10 @@ import (
*/
import "C"
/*
* Gopher server
*/
var (
PreviouslyConnected []string
)
func main() {
/* Setup global logger */
log.SetOutput(os.Stderr)
@ -50,40 +51,52 @@ func main() {
chrootServerDir()
logSystem("Chroot success, new root: %s\n", *ServerRoot)
/* Set-up socket while we still have privileges (if held) */
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *ServerBindAddr, *ServerPort))
if err != nil {
logSystemFatal("Error opening socket on port %d: %s\n", *ServerPort, err.Error())
}
defer listener.Close()
logSystem("Listening: gopher://%s\n", listener.Addr())
/* Setup listeners */
listeners := make([]net.Listener, 0)
/* Set privileges, see function definition for better explanation */
/* If provided unencrypted port, setup listener! */
if *ServerPort != 0 {
l, err := net.Listen("tcp", *ServerBindAddr+":"+strconv.Itoa(*ServerPort))
if err != nil {
logSystemFatal("Error setting up listener on %s: %s\n", *ServerBindAddr+":"+strconv.Itoa(*ServerPort), err.Error())
}
defer l.Close()
logSystem("Listening (unencrypted): gopher://%s\n", l.Addr())
listeners = append(listeners, l)
}
/* Now we've made system calls, drop privileges */
setPrivileges()
/* Handle signals so we can _actually_ shutdowm */
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
/* Compile user restricted files regex if supplied */
compileUserRestrictedFilesRegex()
/* Start file cache system */
startFileCaching()
/* Serve unencrypted traffic */
go func() {
for {
newConn, err := listener.Accept()
if err != nil {
logSystemError("Error accepting connection: %s\n", err.Error())
continue
}
/* Start accepting connections on any supplied listeners */
for _, l := range listeners {
go func() {
for {
newConn, err := l.Accept()
if err != nil {
logSystemError("Error accepting connection: %s\n", err.Error())
continue
}
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
w := NewWorker(&newConn)
w.Serve()
}()
}
}()
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
w := NewWorker(&newConn)
w.Serve()
}()
}
}()
}
/* When OS signal received, we close-up */
sig := <-signals
@ -103,6 +116,12 @@ func chrootServerDir() {
if err != nil {
logSystemFatal("Error chroot'ing into server root %s: %s\n", *ServerRoot, err.Error())
}
/* Change to server root just to ensure we're sitting at root of chroot */
err = syscall.Chdir("/")
if err != nil {
logSystemFatal("Error changing to root of chroot dir: %s\n", err.Error())
}
}
func setPrivileges() {

33
html.go Normal file
View File

@ -0,0 +1,33 @@
package main
/*
func generateHtmlErrorResponse(code ErrorResponseCode) []byte {
content :=
"<html>\n"+
"<body>\n"+
code.String()+"\n"+
"</body>\n"+
"</html>\n"
return generateHttpResponse(code, content)
}
*/
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)
}

37
policy.go Normal file
View File

@ -0,0 +1,37 @@
package main
func generateCapsTxt() []byte {
text := "CAPS"+DOSLineEnd
text += DOSLineEnd
text += "# This is an automatically generated"+DOSLineEnd
text += "# server policy file: caps.txt"+DOSLineEnd
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="+*ServerDescription+DOSLineEnd
text += "ServerGeolocationString="+*ServerGeoloc+DOSLineEnd
text += "ServerDefaultEncoding=ascii"+DOSLineEnd
text += DOSLineEnd
text += "ServerAdmin="+*ServerAdmin+DOSLineEnd
return []byte(text)
}
func generateRobotsTxt() []byte {
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)
}

40
regex.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"regexp"
"strings"
)
var RestrictedFilesRegex []*regexp.Regexp
func compileUserRestrictedFilesRegex() {
if *RestrictedFiles == "" {
/* User not supplied any restricted files, return here */
listDir = _listDir
return
}
/* Try compiling the RestrictedFilesRegex from finalRegex */
logSystem("Compiling restricted file regular expressions\n")
/* Split the user supplied RestrictedFiles string by new-line */
RestrictedFilesRegex = make([]*regexp.Regexp, 0)
for _, expr := range strings.Split(*RestrictedFiles, "\n") {
regex, err := regexp.Compile(expr)
if err != nil {
logSystemFatal("Failed compiling user restricted files regex: %s\n", expr)
}
RestrictedFilesRegex = append(RestrictedFilesRegex, regex)
}
listDir = _listDirRegexMatch
}
func isRestrictedFile(name string) bool {
for _, regex := range RestrictedFilesRegex {
if regex.MatchString(name) {
return true
}
}
return false
}

View File

@ -1,18 +0,0 @@
!Welcome to Gophor!
____ _
/ ___| ___ _ __ | |__ ___ _ __
| | _ / _ \| '_ \| '_ \ / _ \| '__|
| |_| | (_) | |_) | | | | (_) | |
\____|\___/| .__/|_| |_|\___/|_|
|_|
# This is an example of a comment line
# You can place these in your gophermap and Gophor won't serve them
iAbout the owner:
=about.txt
# '=' item type lets you insert text files or 'sub' gophermaps
# Finally you can request that Gophor end and print a directory listing with the '*' item type
*

145
worker.go
View File

@ -1,30 +1,33 @@
package main
import (
"fmt"
"os"
"net"
"path"
"strings"
)
type FileType int
const (
SocketReadBufSize = 256 /* Supplied selector shouldn't be longer than this anyways */
MaxSocketReadChunks = 4
FileReadBufSize = 1024
/* Leads to some more concise code below */
FileTypeRegular FileType = iota
FileTypeDir FileType = iota
FileTypeBad FileType = iota
)
type Worker struct {
Socket net.Conn
Hidden map[string]bool
Socket net.Conn
LogPrefix string
}
func NewWorker(socket *net.Conn) *Worker {
worker := new(Worker)
worker.Socket = *socket
worker.Hidden = map[string]bool{
"gophermap": true,
}
worker.LogPrefix = worker.Socket.RemoteAddr().String()+" "
return worker
}
@ -63,7 +66,6 @@ func (worker *Worker) Serve() {
/* Hit max read chunk size, send error + close connection */
if iter == MaxSocketReadChunks {
worker.SendErrorType("max socket read size reached\n")
logSystemError("Reached max socket read size %d. Closing connection...\n", MaxSocketReadChunks*SocketReadBufSize)
return
}
@ -72,22 +74,25 @@ func (worker *Worker) Serve() {
iter += 1
}
/* Respond */
gophorErr := worker.Respond(received)
/* Handle request */
gophorErr := worker.RespondGopher(received)
/* Handle any error */
if gophorErr != nil {
logSystemError("%s\n", gophorErr.Error())
/* Try 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) SendErrorType(format string, args ...interface{}) {
worker.SendRaw([]byte(fmt.Sprintf(string(TypeError)+"Error: "+format+LastLine, args...)))
}
func (worker *Worker) SendErrorText(format string, args ...interface{}) {
worker.SendRaw([]byte(fmt.Sprintf("Error: "+format, args...)))
}
func (worker *Worker) SendRaw(b []byte) *GophorError {
count, err := worker.Socket.Write(b)
if err != nil {
@ -99,66 +104,53 @@ func (worker *Worker) SendRaw(b []byte) *GophorError {
}
func (worker *Worker) Log(format string, args ...interface{}) {
logAccess(worker.Socket.RemoteAddr().String()+" "+format, args...)
logAccess(worker.LogPrefix+format, args...)
}
func (worker *Worker) LogError(format string, args ...interface{}) {
logAccessError(worker.Socket.RemoteAddr().String()+" "+format, args...)
logAccessError(worker.LogPrefix+format, args...)
}
func (worker *Worker) SanitizePath(dataStr string) string {
/* Clean path and trim '/' prefix if still exists */
requestPath := strings.TrimPrefix(path.Clean(dataStr), "/")
func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* According to Gopher spec, only read up to first Tab or Crlf */
dataStr := readUpToFirstTabOrCrlf(data)
if !strings.HasPrefix(requestPath, "/") {
requestPath = "/" + requestPath
}
return requestPath
}
func (worker *Worker) Respond(data []byte) *GophorError {
/* Only read up to first tab or cr-lf */
dataStr := ""
dataLen := len(data)
for i := 0; i < dataLen; i += 1 {
if data[i] == '\t' {
break
} else if data[i] == DOSLineEnd[0] {
if i == dataLen-1 {
/* Chances are we'll NEVER reach here, still need to check */
return &GophorError{ InvalidRequestErr, nil }
} else if data[i+1] == DOSLineEnd[1] {
break
}
}
dataStr += string(data[i])
/* Handle URL request if so */
lenBefore := len(dataStr)
dataStr = strings.TrimPrefix(dataStr, "URL:")
switch len(dataStr) {
case lenBefore-4:
/* Handle URL prefix */
worker.Log("Redirecting to URL: %s\n", data)
return worker.SendRaw(generateHtmlRedirect(dataStr))
default:
/* Do nothing */
}
/* Sanitize supplied path */
requestPath := worker.SanitizePath(dataStr)
requestPath := sanitizePath(dataStr)
/* Handle policy files */
switch requestPath {
case "/"+CapsTxtStr:
return worker.SendRaw(generateCapsTxt())
case "/"+RobotsTxtStr:
return worker.SendRaw(generateRobotsTxt())
}
/* Open requestPath */
file, err := os.Open(requestPath)
if err != nil {
worker.SendErrorType("read fail\n") /* Purposely vague errors */
return &GophorError{ FileOpenErr, err }
}
/* Leads to some more concise code below */
type FileType int
const(
File FileType = iota
Dir FileType = iota
Bad FileType = iota
)
/* If not empty requestPath, check file type */
fileType := Dir
fileType := FileTypeDir
if requestPath != "." {
stat, err := file.Stat()
if err != nil {
worker.SendErrorType("read fail\n") /* Purposely vague errors */
return &GophorError{ FileStatErr, err }
}
@ -166,9 +158,9 @@ func (worker *Worker) Respond(data []byte) *GophorError {
case stat.Mode() & os.ModeDir != 0:
// do nothing :)
case stat.Mode() & os.ModeType == 0:
fileType = File
fileType = FileTypeRegular
default:
fileType = Bad
fileType = FileTypeBad
}
}
@ -181,7 +173,7 @@ func (worker *Worker) Respond(data []byte) *GophorError {
response := make([]byte, 0)
switch fileType {
/* Directory */
case Dir:
case FileTypeDir:
/* First try to serve gopher map */
gophermapPath := path.Join(requestPath, "/"+GophermapFileStr)
fileContents, gophorErr := GlobalFileCache.FetchGophermap(gophermapPath)
@ -189,7 +181,6 @@ func (worker *Worker) Respond(data []byte) *GophorError {
/* Get directory listing instead */
fileContents, gophorErr = listDir(requestPath, map[string]bool{})
if gophorErr != nil {
worker.SendErrorType("dir list failed\n")
return gophorErr
}
@ -206,11 +197,10 @@ func (worker *Worker) Respond(data []byte) *GophorError {
}
/* Regular file */
case File:
case FileTypeRegular:
/* Read file contents */
fileContents, gophorErr := GlobalFileCache.FetchRegular(requestPath)
if gophorErr != nil {
worker.SendErrorText("file read fail\n")
return gophorErr
}
@ -229,3 +219,34 @@ func (worker *Worker) Respond(data []byte) *GophorError {
/* Serve response */
return worker.SendRaw(response)
}
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 {
if data[i] == '\t' {
break
} else if data[i] == DOSLineEnd[0] {
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
/* Finished on Unix line end, NOT DOS */
break
}
}
dataStr += string(data[i])
}
return dataStr
}
func sanitizePath(dataStr string) string {
/* Clean path and trim '/' prefix if still exists */
requestPath := strings.TrimPrefix(path.Clean(dataStr), "/")
if !strings.HasPrefix(requestPath, "/") {
requestPath = "/" + requestPath
}
return requestPath
}