commit
158e79aea5
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
gophor
|
||||
*.log
|
||||
*.old
|
||||
build/
|
210
README.md
210
README.md
@ -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
80
build-all.sh
Executable 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..."
|
120
cache.go
120
cache.go
@ -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()
|
||||
|
@ -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
|
||||
|
94
error.go
94
error.go
@ -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
73
fixedmap.go
Normal 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)
|
||||
}
|
34
flags.go
34
flags.go
@ -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).")
|
||||
)
|
||||
|
80
format.go
80
format.go
@ -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
88
fs.go
@ -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] }
|
||||
|
16
gophermap.go
16
gophermap.go
@ -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
|
||||
|
73
gophor.go
73
gophor.go
@ -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
33
html.go
Normal 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
37
policy.go
Normal 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
40
regex.go
Normal 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
|
||||
}
|
@ -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
145
worker.go
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user