inline shell commands, executable gophermaps, initial cgi-bin support

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
This commit is contained in:
kim (grufwub) 2020-05-03 22:42:51 +01:00
parent 01eb36a814
commit eac520031a
15 changed files with 597 additions and 411 deletions

View File

@ -11,17 +11,21 @@ import (
* and file cache)
*/
type ServerConfig struct {
/* Base settings */
/* Executable Settings */
Env []string
/* Content settings */
FooterText []byte
PageWidth int
RestrictedFiles []*regexp.Regexp
FooterText []byte
PageWidth int
/* Regex */
CmdParseLineRegex *regexp.Regexp
RestrictedFiles []*regexp.Regexp
/* Logging */
SysLog LoggerInterface
AccLog LoggerInterface
SysLog LoggerInterface
AccLog LoggerInterface
/* Filesystem access */
FileSystem *FileSystem
FileSystem *FileSystem
}

View File

@ -1,97 +0,0 @@
package main
const (
/* Gophor */
GophorVersion = "0.6-beta-PR2"
/* Socket settings */
SocketReadBufSize = 256 /* Supplied selector shouldn't be longer than this anyways */
MaxSocketReadChunks = 1
FileReadBufSize = 1024
/* Parsing */
DOSLineEnd = "\r\n"
UnixLineEnd = "\n"
End = "."
Tab = "\t"
LastLine = End+DOSLineEnd
/* Line creation */
MaxUserNameLen = 70 /* RFC 1436 standard */
MaxSelectorLen = 255 /* RFC 1436 standard */
NullSelector = "-"
NullHost = "null.host"
NullPort = "0"
SelectorErrorStr = "selector_length_error"
GophermapRenderErrorStr = ""
/* Replacement strings */
ReplaceStrHostname = "$hostname"
ReplaceStrPort = "$port"
/* Filesystem */
GophermapFileStr = "gophermap"
CapsTxtStr = "caps.txt"
RobotsTxtStr = "robots.txt"
/* Misc */
BytesInMegaByte = 1048576.0
)
/*
* Item type characters:
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
* and Gophernicus project. Those with ALL-CAPS descriptions in
* [square brackets] defined and used by Gophernicus, a popular
* Gopher server.
*/
type ItemType byte
const (
/* RFC 1436 Standard */
TypeFile = ItemType('0') /* Regular file (text) */
TypeDirectory = ItemType('1') /* Directory (menu) */
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
TypeError = ItemType('3') /* Error message */
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
TypeSearch = ItemType('7') /* Query search engine or CGI script */
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
TypeGif = ItemType('g') /* GIF format image file (just use I) */
TypeImage = ItemType('I') /* Any format image file */
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
/* GopherII Standard */
TypeCalendar = ItemType('c') /* Calendar file */
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
TypeHtml = ItemType('h') /* HTML document */
TypeInfo = ItemType('i') /* Informational text (not selectable) */
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
TypeMail = ItemType('M') /* Email repository (MBOX) */
TypeAudio = ItemType('s') /* Audio recordings */
TypeXml = ItemType('x') /* eXtensible Markup Language document */
TypeVideo = ItemType(';') /* Video files */
/* Commonly Used */
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on a directory listing */
/* Planned To Be Supported */
TypeExec = ItemType('$') /* [SERVER ONLY] Execute shell command and print stdout here */
/* Default type */
TypeDefault = TypeBin
/* Gophor specific types */
TypeInfoNotStated = ItemType('z') /* [INTERNAL USE] */
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
)

View File

@ -16,7 +16,7 @@ const (
FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota
/* Sockets */
SocketWriteErr ErrorCode = iota
SocketWriteCountErr ErrorCode = iota
@ -24,9 +24,12 @@ const (
/* Parsing */
InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr ErrorCode = iota
EntityPortParseErr ErrorCode = iota
InvalidGophermapErr ErrorCode = iota
/* Executing */
BufferReadErr ErrorCode = iota
CommandStartErr ErrorCode = iota
/* Error Response Codes */
ErrorResponse200 ErrorResponseCode = iota
ErrorResponse400 ErrorResponseCode = iota
@ -75,11 +78,14 @@ func (e *GophorError) Error() string {
str = "invalid request data"
case EmptyItemTypeErr:
str = "line string provides no dir entity type"
case EntityPortParseErr:
str = "parsing dir entity port"
case InvalidGophermapErr:
str = "invalid gophermap"
case BufferReadErr:
str = "buffer read fail"
case CommandStartErr:
str = "command start fail"
default:
str = "Unknown"
}
@ -120,11 +126,14 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
return ErrorResponse400
case EmptyItemTypeErr:
return ErrorResponse500
case EntityPortParseErr:
return ErrorResponse500
case InvalidGophermapErr:
return ErrorResponse500
case BufferReadErr:
return ErrorResponse500
case CommandStartErr:
return ErrorResponse500
default:
return ErrorResponse503
}

104
exec.go Normal file
View File

@ -0,0 +1,104 @@
package main
import (
"os/exec"
"syscall"
"bytes"
"io"
)
const (
/* Reader buf size */
ReaderBufSize = 1024
)
func executeFile(requestPath *RequestPath, args []string) ([]byte, *GophorError) {
/* Create stdout, stderr buffers */
outBuffer := &bytes.Buffer{}
errBuffer := &bytes.Buffer{}
/* Setup command */
var cmd *exec.Cmd
if args != nil {
cmd = exec.Command(requestPath.AbsolutePath(), args...)
} else {
cmd = exec.Command(requestPath.AbsolutePath())
}
/* Setup operating environment */
cmd.Env = Config.Env /* User defined cgi-bin environment */
/* Set buffers*/
cmd.Stdout = outBuffer
cmd.Stderr = errBuffer
/* Start executing! */
err := cmd.Start()
if err != nil {
return nil, &GophorError{ CommandStartErr, err }
}
/* Wait for command to finish, get exit code */
err = cmd.Wait()
exitCode := 0
if err != nil {
/* Error, try to get exit code */
exitError, ok := err.(*exec.ExitError)
if ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
} else {
exitCode = 1
}
} else {
/* No error! Get exit code direct from command */
waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
}
if exitCode != 0 {
/* If non-zero exit code return error, print stderr to sys log */
errContents, gophorErr := readBuffer(errBuffer)
if gophorErr == nil {
/* Only print if we successfully fetched errContents */
Config.SysLog.Error("", "Error executing: %s %v\n%s\n", requestPath.AbsolutePath(), args, errContents)
}
return nil, &GophorError{ }
} else {
/* If zero exit code try return outContents and no error */
outContents, gophorErr := readBuffer(outBuffer)
if gophorErr != nil {
/* Failed fetching outContents, return error */
return nil, &GophorError{ }
}
return outContents, nil
}
}
func readBuffer(reader *bytes.Buffer) ([]byte, *GophorError) {
var err error
var count int
contents := make([]byte, 0)
buf := make([]byte, ReaderBufSize)
for {
count, err = reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return nil, &GophorError{ BufferReadErr, err }
}
contents = append(contents, buf[:count]...)
if count < ReaderBufSize {
break
}
}
return contents, nil
}

View File

@ -3,6 +3,7 @@ package main
import (
"bytes"
"bufio"
"os"
)
/* GeneratedFileContents:
@ -146,6 +147,24 @@ func (s *GophermapDirListing) Render(request *FileSystemRequest) ([]byte, *Gopho
return listDir(&FileSystemRequest{ s.Path, request.Host }, s.Hidden)
}
/* GophermapExecutable:
* An implementation of GophermapSection that holds onto a path,
* and a string slice of arguments for the supplied executable path.
*/
type GophermapExecutable struct {
Path *RequestPath
Args []string
}
func NewGophermapExecutable(path *RequestPath, args []string) *GophermapExecutable {
return &GophermapExecutable{ path, args }
}
func (s *GophermapExecutable) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return executeFile(s.Path, s.Args)
}
func readGophermap(requestPath *RequestPath) ([]GophermapSection, *GophorError) {
/* Create return slice */
sections := make([]GophermapSection, 0)
@ -187,40 +206,49 @@ func readGophermap(requestPath *RequestPath) ([]GophermapSection, *GophorError)
hidden[line[1:]] = true
case TypeSubGophermap:
/* Check if we've been supplied subgophermap or regular file */
if requestPath.HasRelativeSuffix(GophermapFileStr) {
/* Ensure we haven't been passed the current gophermap. Recursion bad! */
if line[1:] == requestPath.RelativePath() {
break
}
/* Treat as any other gopher map! */
subPath := requestPath.NewPathAtRoot(line[1:])
submapSections, gophorErr := readGophermap(subPath)
if gophorErr != nil {
/* Failed to read subgophermap, insert error line */
sections = append(sections, NewGophermapText(buildInfoLine("Error reading subgophermap: "+line[1:])))
} else {
sections = append(sections, submapSections...)
}
} else {
/* Treat as regular file, but we need to replace Unix line endings
* with gophermap line endings
*/
subPath := requestPath.NewPathAtRoot(line[1:])
fileContents, gophorErr := readIntoGophermap(subPath.AbsolutePath())
if gophorErr != nil {
/* Failed to read file, insert error line */
Config.SysLog.Info("", "Error: %s\n", gophorErr)
sections = append(sections, NewGophermapText(buildInfoLine("Error reading subgophermap: "+line[1:])))
} else {
sections = append(sections, NewGophermapText(fileContents))
}
/* Create new request path and args array */
subPath, args := parseLineFileSystemRequest(requestPath.Root, line[1:])
if !subPath.HasAbsolutePrefix("/") {
/* Special case here where command must be in path, return GophermapExecutable */
sections = append(sections, NewGophermapExecutable(subPath, args))
} else if subPath.RelativePath() == "" {
/* path cleaning failed */
break
} else if subPath.RelativePath() == requestPath.RelativePath() {
/* Same as current gophermap. Recursion bad! */
break
}
case TypeExec:
/* Try executing supplied line */
sections = append(sections, NewGophermapText(buildInfoLine("Error: inline shell commands not yet supported")))
/* Perform file stat */
stat, err := os.Stat(subPath.AbsolutePath())
if (err != nil) || (stat.Mode() & os.ModeDir != 0) {
/* File read error or is directory */
break
}
/* Check if we've been supplied subgophermap or regular file */
if subPath.HasAbsoluteSuffix("/"+GophermapFileStr) {
/* If executable, store as GophermapExecutable, else readGophermap() */
if stat.Mode().Perm() & 0100 != 0 {
sections = append(sections, NewGophermapExecutable(subPath, args))
} else {
/* Treat as any other gophermap! */
submapSections, gophorErr := readGophermap(subPath)
if gophorErr == nil {
sections = append(sections, submapSections...)
}
}
} else {
/* If stored in cgi-bin store as GophermapExecutable, else read into GophermapText */
if subPath.HasRelativePrefix(CgiBinDirStr) {
sections = append(sections, NewGophermapExecutable(subPath, args))
} else {
fileContents, gophorErr := readIntoGophermap(subPath.AbsolutePath())
if gophorErr == nil {
sections = append(sections, NewGophermapText(fileContents))
}
}
}
case TypeEnd:
/* Lastline, break out at end of loop. Interface method Contents()
@ -239,7 +267,7 @@ func readGophermap(requestPath *RequestPath) ([]GophermapSection, *GophorError)
/* Just append to sections slice as gophermap text */
sections = append(sections, NewGophermapText([]byte(line+DOSLineEnd)))
}
return true
},
)
@ -287,7 +315,7 @@ func readIntoGophermap(path string) ([]byte, *GophorError) {
fileContents = append(fileContents, buildInfoLine(line[:length])...)
line = line[length:]
}
return true
},
)

View File

@ -8,10 +8,12 @@ import (
type FileType int
const (
/* Leads to some more concise code below */
FileTypeRegular FileType = iota
FileTypeDir FileType = iota
FileTypeBad FileType = iota
/* Help converting file size stat to supplied size in megabytes */
BytesInMegaByte = 1048576.0
/* Filename constants */
CgiBinDirStr = "cgi-bin"
GophermapFileStr = "gophermap"
)
/* FileSystem:
@ -32,7 +34,7 @@ func (fs *FileSystem) Init(size int, fileSizeMax float64) {
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
}
func (fs *FileSystem) HandleRequest(requestPath *RequestPath, host *ConnHost) ([]byte, *GophorError) {
func (fs *FileSystem) HandleRequest(host *ConnHost, requestPath *RequestPath, args []string) ([]byte, *GophorError) {
/* Get absolute path */
absPath := requestPath.AbsolutePath()
@ -56,32 +58,28 @@ func (fs *FileSystem) HandleRequest(requestPath *RequestPath, host *ConnHost) ([
return b, nil
}
/* Using stat, set file type for later handling */
var fileType FileType
switch {
case stat.Mode() & os.ModeDir != 0:
fileType = FileTypeDir
case stat.Mode() & os.ModeType == 0:
fileType = FileTypeRegular
default:
fileType = FileTypeBad
}
/* Handle file type */
switch fileType {
switch {
/* Directory */
case FileTypeDir:
case stat.Mode() & os.ModeDir != 0:
/* Ignore cgi-bin directory */
if requestPath.HasRelativePrefix(CgiBinDirStr) {
return nil, &GophorError{ IllegalPathErr, nil }
}
/* Check Gophermap exists */
gophermapPath := requestPath.NewJoinPathFromCurrent(GophermapFileStr)
_, err := os.Stat(gophermapPath.AbsolutePath())
stat, err = os.Stat(gophermapPath.AbsolutePath())
var output []byte
var gophorErr *GophorError
if err == nil {
/* Gophermap exists, update requestPath and serve this! */
output, gophorErr = fs.FetchFile(&FileSystemRequest{ gophermapPath, host })
/* Gophermap exists! If executable execute, else serve. */
if stat.Mode().Perm() & 0100 != 0 {
output, gophorErr = executeFile(gophermapPath, args)
} else {
output, gophorErr = fs.FetchFile(&FileSystemRequest{ gophermapPath, host })
}
} else {
/* No gophermap, serve directory listing */
output, gophorErr = listDir(&FileSystemRequest{ requestPath, host }, map[string]bool{})
@ -97,8 +95,13 @@ func (fs *FileSystem) HandleRequest(requestPath *RequestPath, host *ConnHost) ([
return output, nil
/* Regular file */
case FileTypeRegular:
return fs.FetchFile(&FileSystemRequest{ requestPath, host })
case stat.Mode() & os.ModeType == 0:
/* If cgi-bin, return executed contents. Else, fetch */
if requestPath.HasRelativePrefix(CgiBinDirStr) {
return executeFile(requestPath, args)
} else {
return fs.FetchFile(&FileSystemRequest{ requestPath, host })
}
/* Unsupported type */
default:
@ -145,7 +148,7 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
return nil, &GophorError{ FileStatErr, err }
}
/* Create new file contents object using supplied function */
/* Create new file contents */
var contents FileContents
if request.Path.HasAbsoluteSuffix("/"+GophermapFileStr) {
contents = &GophermapContents{ request.Path, nil }

View File

@ -8,6 +8,10 @@ import (
"bufio"
)
const (
FileReadBufSize = 1024
)
/* Perform simple buffered read on a file at path */
func bufferedRead(path string) ([]byte, *GophorError) {
/* Open file */
@ -106,69 +110,8 @@ func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, er
return 0, nil, nil
}
/* 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(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError)
func _listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If requested hidden */
if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := request.Path.JoinSelectorPath(file.Name())
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := request.Path.JoinSelectorPath(file.Name())
itemType := getItemType(itemPath)
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
})
}
func _listDirRegexMatch(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
return _listDirBase(request, func(dirContents *[]byte, file os.FileInfo) {
/* If regex match in restricted files || requested hidden */
if isRestrictedFile(file.Name()) {
return
} else if _, ok := hidden[file.Name()]; ok {
return
}
/* Handle file, directory or ignore others */
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := request.Path.JoinSelectorPath(file.Name())
*dirContents = append(*dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := request.Path.JoinSelectorPath(file.Name())
itemType := getItemType(itemPath)
*dirContents = append(*dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
})
}
func _listDirBase(request *FileSystemRequest, iterFunc func(dirContents *[]byte, file os.FileInfo)) ([]byte, *GophorError) {
/* List the files in a directory, hiding those requested */
func listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
/* Open directory file descriptor */
fd, err := os.Open(request.Path.AbsolutePath())
if err != nil {
@ -197,7 +140,31 @@ func _listDirBase(request *FileSystemRequest, iterFunc func(dirContents *[]byte,
dirContents = append(dirContents, buildLine(TypeDirectory, "..", request.Path.JoinRelativePath(".."), request.Host.Name, request.Host.Port)...)
/* Walk through files :D */
for _, file := range files { iterFunc(&dirContents, file) }
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 := request.Path.JoinSelectorPath(file.Name())
dirContents = append(dirContents, buildLine(TypeDirectory, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
case file.Mode() & os.ModeType == 0:
/* Regular file -- find item type and creating listing */
itemPath := request.Path.JoinSelectorPath(file.Name())
itemType := getItemType(itemPath)
dirContents = append(dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
default:
/* Ignore */
}
}
return dirContents, nil
}

View File

@ -74,25 +74,37 @@ func (rp *RequestPath) JoinAbsolutePath(extPath string) string {
}
func (rp *RequestPath) JoinRelativePath(extPath string) string {
return path.Join(rp.Path, extPath)
return path.Join(rp.RelativePath(), extPath)
}
func (rp *RequestPath) HasAbsolutePrefix(prefix string) bool {
return strings.HasPrefix(rp.AbsolutePath(), prefix)
}
func (rp *RequestPath) HasRelativePrefix(prefix string) bool {
return strings.HasPrefix(rp.RelativePath(), prefix)
}
func (rp *RequestPath) HasRelativeSuffix(suffix string) bool {
return strings.HasSuffix(rp.RelativePath(), suffix)
}
func (rp *RequestPath) HasAbsoluteSuffix(suffix string) bool {
return strings.HasSuffix(rp.AbsolutePath(), suffix)
}
func (rp *RequestPath) HasRelativeSuffix(suffix string) bool {
return strings.HasSuffix(rp.Path, suffix)
}
func (rp *RequestPath) TrimRelativeSuffix(suffix string) string {
return strings.TrimSuffix(rp.Path, suffix)
return strings.TrimSuffix(rp.RelativePath(), suffix)
}
func (rp *RequestPath) TrimAbsoluteSuffix(suffix string) string {
return strings.TrimSuffix(rp.AbsolutePath(), suffix)
}
func (rp *RequestPath) JoinPathFromRoot(extPath string) string {
return path.Join(rp.Root, extPath)
}
func (rp *RequestPath) NewJoinPathFromCurrent(extPath string) *RequestPath {
/* DANGER THIS DOES NOT CHECK FOR BACK-DIR TRAVERSALS */
return NewRequestPath(rp.Root, rp.JoinRelativePath(extPath))

127
format.go
View File

@ -4,6 +4,88 @@ import (
"strings"
)
const (
/* Just naming some constants */
DOSLineEnd = "\r\n"
UnixLineEnd = "\n"
End = "."
Tab = "\t"
LastLine = End+DOSLineEnd
/* Gopher line formatting */
MaxUserNameLen = 70 /* RFC 1436 standard, though we use user-supplied page-width */
MaxSelectorLen = 255 /* RFC 1436 standard */
SelectorErrorStr = "/max_selector_length_reached"
GophermapRenderErrorStr = ""
GophermapReadErrorStr = "Error reading subgophermap: "
GophermapExecErrorStr = "Error executing gophermap: "
/* Default null values */
NullSelector = "-"
NullHost = "null.host"
NullPort = "0"
/* Replacement strings */
ReplaceStrHostname = "$hostname"
ReplaceStrPort = "$port"
)
/*
* Item type characters:
* Collected from RFC 1436 standard, Wikipedia, Go-gopher project
* and Gophernicus project. Those with ALL-CAPS descriptions in
* [square brackets] defined and used by Gophernicus, a popular
* Gopher server.
*/
type ItemType byte
const (
/* RFC 1436 Standard */
TypeFile = ItemType('0') /* Regular file (text) */
TypeDirectory = ItemType('1') /* Directory (menu) */
TypeDatabase = ItemType('2') /* CCSO flat db; other db */
TypeError = ItemType('3') /* Error message */
TypeMacBinHex = ItemType('4') /* Macintosh BinHex file */
TypeBinArchive = ItemType('5') /* Binary archive (zip, rar, 7zip, tar, gzip, etc), CLIENT MUST READ UNTIL TCP CLOSE */
TypeUUEncoded = ItemType('6') /* UUEncoded archive */
TypeSearch = ItemType('7') /* Query search engine or CGI script */
TypeTelnet = ItemType('8') /* Telnet to: VT100 series server */
TypeBin = ItemType('9') /* Binary file (see also, 5), CLIENT MUST READ UNTIL TCP CLOSE */
TypeTn3270 = ItemType('T') /* Telnet to: tn3270 series server */
TypeGif = ItemType('g') /* GIF format image file (just use I) */
TypeImage = ItemType('I') /* Any format image file */
TypeRedundant = ItemType('+') /* Redundant (indicates mirror of previous item) */
/* GopherII Standard */
TypeCalendar = ItemType('c') /* Calendar file */
TypeDoc = ItemType('d') /* Word-processing document; PDF document */
TypeHtml = ItemType('h') /* HTML document */
TypeInfo = ItemType('i') /* Informational text (not selectable) */
TypeMarkup = ItemType('p') /* Page layout or markup document (plain text w/ ASCII tags) */
TypeMail = ItemType('M') /* Email repository (MBOX) */
TypeAudio = ItemType('s') /* Audio recordings */
TypeXml = ItemType('x') /* eXtensible Markup Language document */
TypeVideo = ItemType(';') /* Video files */
/* Commonly Used */
TypeTitle = ItemType('!') /* [SERVER ONLY] Menu title (set title ONCE per gophermap) */
TypeComment = ItemType('#') /* [SERVER ONLY] Comment, rest of line is ignored */
TypeHiddenFile = ItemType('-') /* [SERVER ONLY] Hide file/directory from directory listing */
TypeEnd = ItemType('.') /* [SERVER ONLY] Last line -- stop processing gophermap default */
TypeSubGophermap = ItemType('=') /* [SERVER ONLY] Include subgophermap / regular file here. */
TypeEndBeginList = ItemType('*') /* [SERVER ONLY] Last line + directory listing -- stop processing gophermap and end on directory listing */
/* Planned To Be Supported */
TypeExec = ItemType('$') /* [SERVER ONLY] Execute shell command and print stdout here */
/* Default type */
TypeDefault = TypeBin
/* Gophor specific types */
TypeInfoNotStated = ItemType('z') /* [INTERNAL USE] */
TypeUnknown = ItemType('?') /* [INTERNAL USE] */
)
var FileExtMap = map[string]ItemType{
".out": TypeBin,
".a": TypeBin,
@ -166,51 +248,6 @@ func formatGophermapFooter(text string, useSeparator bool) []byte {
return ret
}
/* Parse line type from contents */
func parseLineType(line string) ItemType {
lineLen := len(line)
if lineLen == 0 {
return TypeInfoNotStated
} else if lineLen == 1 {
/* The only accepted types for a length 1 line */
switch ItemType(line[0]) {
case TypeEnd:
return TypeEnd
case TypeEndBeginList:
return TypeEndBeginList
case TypeComment:
return TypeComment
case TypeInfo:
return TypeInfo
case TypeTitle:
return TypeTitle
default:
return TypeUnknown
}
} else if !strings.Contains(line, string(Tab)) {
/* The only accepted types for a line with no tabs */
switch ItemType(line[0]) {
case TypeComment:
return TypeComment
case TypeTitle:
return TypeTitle
case TypeInfo:
return TypeInfo
case TypeHiddenFile:
return TypeHiddenFile
case TypeSubGophermap:
return TypeSubGophermap
case TypeExec:
return TypeExec
default:
return TypeInfoNotStated
}
}
return ItemType(line[0])
}
/* Replace standard replacement strings */
func replaceStrings(str string, connHost *ConnHost) []byte {
str = strings.Replace(str, ReplaceStrHostname, connHost.Name, -1)

119
gophor.go
View File

@ -2,7 +2,6 @@ package main
import (
"os"
"os/user"
"strconv"
"syscall"
"os/signal"
@ -10,23 +9,9 @@ import (
"time"
)
/*
* GoLang's built-in syscall.{Setuid,Setgid}() methods don't work as expected (all I ever
* run into is 'operation not supported'). Which from reading seems to be a result of Linux
* not always performing setuid/setgid constistent with the Unix expected result. Then mix
* that with GoLang's goroutines acting like threads but not quite the same... I can see
* why they're not fully supported.
*
* Instead we're going to take C-bindings and call them directly ourselves, BEFORE spawning
* any goroutines to prevent fuckery.
*
* Oh god here we go...
*/
/*
#include <unistd.h>
*/
import "C"
const (
GophorVersion = "0.6-beta-PR3"
)
var (
Config *ServerConfig
@ -74,8 +59,7 @@ func setupServer() []*GophorListener {
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")
execAs := flag.String("user", "", "Drop to supplied user's UID and GID permissions before execution.")
rootless := flag.Bool("rootless", false, "Run without root privileges (no chroot, no privilege drop, no restricted ports).")
serverEnv := flag.String("env", "", "New-line separated list of environment variables to be used when executing moles.")
/* User supplied caps.txt information */
serverDescription := flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in generated caps.txt.")
@ -121,50 +105,21 @@ func setupServer() []*GophorListener {
Config.SysLog, Config.AccLog = setupLoggers(*logOutput, *logOpts, *systemLogPath, *accessLogPath)
/* If running as root, get ready to drop privileges */
var uid, gid int
if !*rootless {
/* Getting UID+GID for supplied user, has to be done BEFORE chroot */
if *execAs == "" {
/* No user supplied :( */
Config.SysLog.Fatal("", "Gophor requires a supplied user name to drop privileges to.\n")
} else if *execAs == "root" {
/* Naughty, naughty! */
Config.SysLog.Fatal("", "Gophor does not support directly running as root, please supply a non-root user account\n")
} else {
/* Try lookup specified username */
user, err := user.Lookup(*execAs)
if err != nil {
Config.SysLog.Fatal("", "Error getting information for requested user %s: %s\n", *execAs, err)
}
/* These values should be coming straight out of /etc/passwd, so assume safe */
uid, _ = strconv.Atoi(user.Uid)
gid, _ = strconv.Atoi(user.Gid)
/* Double check this isn't a privileged account */
if uid == 0 || gid == 0 {
Config.SysLog.Info("", "Gophor does not support running with any kind of privileges, please supply a non-root user account\n")
}
}
} else {
if syscall.Getuid() == 0 || syscall.Getgid() == 0 {
Config.SysLog.Info("", "Gophor (obviously) does not support running rootless, as root -.-\n")
} else if *execAs != "" {
Config.SysLog.Info("", "Gophor does not support dropping privileges when running rootless\n")
}
if syscall.Getuid() == 0 || syscall.Getgid() == 0 {
Config.SysLog.Fatal("", "Gophor does not support running as root!\n")
}
/* Enter server dir */
enterServerDir(*serverRoot)
Config.SysLog.Info("", "Entered server directory: %s\n", *serverRoot)
/* Try enter chroot if requested */
if *rootless {
Config.SysLog.Info("", "Running rootless, server root set: %s\n", *serverRoot)
/* Setup server shell environment */
if *serverEnv != "" {
Config.Env = parseEnvironmentString(*serverEnv)
Config.SysLog.Info("", "Using user supplied shell environment\n")
} else {
chrootServerDir(*serverRoot)
*serverRoot = "/"
Config.SysLog.Info("", "Chroot success, new root: %s\n", *serverRoot)
Config.Env = os.Environ()
Config.SysLog.Info("", "Using call process shell environment\n")
}
/* Setup listeners */
@ -181,25 +136,12 @@ func setupServer() []*GophorListener {
Config.SysLog.Fatal("", "No valid port to listen on\n")
}
/* Drop not rootless, privileges to retrieved UID+GID */
if !*rootless {
setPrivileges(uid, gid)
Config.SysLog.Info("", "Successfully dropped privileges to UID:%d GID:%d\n", uid, gid)
} else {
Config.SysLog.Info("", "Running as current user\n")
}
/* Compile CmdParse regular expression */
Config.CmdParseLineRegex = compileCmdParseRegex()
/* Compile user restricted files regex if supplied */
if *restrictedFiles != "" {
Config.RestrictedFiles = compileUserRestrictedFilesRegex(*restrictedFiles)
Config.SysLog.Info("", "Restricted files regular expressions compiled\n")
/* Setup the listDir function to use regex matching */
listDir = _listDirRegexMatch
} else {
/* Setup the listDir function to skip regex matching */
listDir = _listDir
}
/* Compile user restricted files regex */
Config.RestrictedFiles = compileUserRestrictedFilesRegex(*restrictedFiles)
Config.SysLog.Info("", "Compiled restricted files regular expressions\n")
/* Setup file cache */
Config.FileSystem = new(FileSystem)
@ -242,30 +184,3 @@ func enterServerDir(path string) {
Config.SysLog.Fatal("", "Error changing dir to server root %s: %s\n", path, err.Error())
}
}
func chrootServerDir(path string) {
err := syscall.Chroot(path)
if err != nil {
Config.SysLog.Fatal("", "Error chroot'ing into server root %s: %s\n", path, err.Error())
}
/* Change to server root just to ensure we're sitting at root of chroot */
err = syscall.Chdir("/")
if err != nil {
Config.SysLog.Fatal("", "Error changing to root of chroot dir: %s\n", err.Error())
}
}
func setPrivileges(execUid, execGid int) {
/* C-bind setgid */
result := C.setgid(C.uint(execGid))
if result != 0 {
Config.SysLog.Fatal("", "Failed setting GID %d: %d\n", execGid, result)
}
/* C-bind setuid */
result = C.setuid(C.uint(execUid))
if result != 0 {
Config.SysLog.Fatal("", "Failed setting UID %d: %d\n", execUid, result)
}
}

View File

@ -22,17 +22,20 @@ const (
LogIps = "ip"
)
/* Defines a simple logger interface */
type LoggerInterface interface {
Info(string, string, ...interface{})
Error(string, string, ...interface{})
Fatal(string, string, ...interface{})
}
/* Logger interface definition that does jack-shit */
type NullLogger struct {}
func (l *NullLogger) Info(prefix, format string, args ...interface{}) {}
func (l *NullLogger) Error(prefix, format string, args ...interface{}) {}
func (l *NullLogger) Fatal(prefix, format string, args ...interface{}) {}
/* A basic logger implemention */
type Logger struct {
Logger *log.Logger
}
@ -53,6 +56,7 @@ type LoggerNoPrefix struct {
Logger *log.Logger
}
/* Logger implementation that ignores the prefix (e.g. when not printing IPs) */
func (l *LoggerNoPrefix) Info(prefix, format string, args ...interface{}) {
/* Ignore the prefix */
l.Logger.Printf(LogPrefixInfo+format, args...)
@ -68,7 +72,9 @@ func (l *LoggerNoPrefix) Fatal(prefix, format string, args ...interface{}) {
l.Logger.Fatalf(LogPrefixFatal+format, args...)
}
/* Setup the system and access logger interfaces according to supplied output options and logger options */
func setupLoggers(logOutput, logOpts, systemLogPath, accessLogPath string) (LoggerInterface, LoggerInterface) {
/* Parse the logger options */
logIps := false
logFlags := 0
for _, opt := range strings.Split(logOpts, ",") {
@ -87,6 +93,7 @@ func setupLoggers(logOutput, logOpts, systemLogPath, accessLogPath string) (Logg
}
}
/* Setup the loggers according to requested logging output */
switch logOutput {
case "":
/* Assume empty means stderr */
@ -122,10 +129,12 @@ func setupLoggers(logOutput, logOpts, systemLogPath, accessLogPath string) (Logg
}
/* Helper function to create new standard log.Logger to stderr */
func NewLoggerToStderr(logFlags int) *log.Logger {
return log.New(os.Stderr, "", logFlags)
}
/* Helper function to create new standard log.Logger to file */
func NewLoggerToFile(path string, logFlags int) *log.Logger {
writer, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
@ -135,7 +144,7 @@ func NewLoggerToFile(path string, logFlags int) *log.Logger {
}
func printVersionExit() {
/* Reset the flags before printing version */
/* Set the default logger flags before printing version */
log.SetFlags(0)
log.Printf("%s\n", GophorVersion)
os.Exit(0)

176
parse.go Normal file
View File

@ -0,0 +1,176 @@
package main
import (
"strings"
"path"
"net/url"
)
/* Parse line type from contents */
func parseLineType(line string) ItemType {
lineLen := len(line)
if lineLen == 0 {
return TypeInfoNotStated
} else if lineLen == 1 {
/* The only accepted types for a length 1 line */
switch ItemType(line[0]) {
case TypeEnd:
return TypeEnd
case TypeEndBeginList:
return TypeEndBeginList
case TypeComment:
return TypeComment
case TypeInfo:
return TypeInfo
case TypeTitle:
return TypeTitle
default:
return TypeUnknown
}
} else if !strings.Contains(line, string(Tab)) {
/* The only accepted types for a line with no tabs */
switch ItemType(line[0]) {
case TypeComment:
return TypeComment
case TypeTitle:
return TypeTitle
case TypeInfo:
return TypeInfo
case TypeHiddenFile:
return TypeHiddenFile
case TypeSubGophermap:
return TypeSubGophermap
case TypeExec:
return TypeExec
default:
return TypeInfoNotStated
}
}
return ItemType(line[0])
}
/* Parses a line in a gophermap into a filesystem request path and a string slice of arguments */
func parseLineFileSystemRequest(rootDir, requestStr string) (*RequestPath, []string) {
if path.IsAbs(requestStr) {
/* This is an absolute path, assume it must be within gopher directory */
args := splitLineStringArgs(requestStr)
requestPath := NewSanitizedRequestPath(rootDir, args[0])
if len(args) > 1 {
return requestPath, args[1:]
} else {
return requestPath, nil
}
} else {
/* Not an absolute path, if starts with cgi-bin treat as within gopher directory, else as command in path */
if strings.HasPrefix(requestStr, CgiBinDirStr) {
args := splitLineStringArgs(requestStr)
requestPath := NewSanitizedRequestPath(rootDir, args[0])
if len(args) > 1 {
return requestPath, args[1:]
} else {
return requestPath, nil
}
} else {
args := splitLineStringArgs(requestStr)
/* Manually create specialised request path */
requestPath := NewRequestPath(args[0], "")
if len(args) > 1 {
return requestPath, args[1:]
} else {
return requestPath, nil
}
}
}
}
/* Parses a gopher request string into a filesystem request path and string slice of arguments */
func parseFileSystemRequest(rootDir, requestStr string) (*RequestPath, []string, *GophorError) {
/* Split the request string */
args := splitRequestStringArgs(requestStr)
/* Now URL decode all the parts. */
var err error
for i := range args {
args[i], err = url.QueryUnescape(args[i])
if err != nil {
return nil, nil, &GophorError{ InvalidRequestErr, err }
}
}
/* Create request path */
requestPath := NewSanitizedRequestPath(rootDir, args[0])
/* Return request path and args if precent */
if len(args) > 1 {
return requestPath, args[1:], nil
} else {
return requestPath, nil, nil
}
}
/* Parse new-line separated string of environment variables into a slice */
func parseEnvironmentString(env string) []string {
return splitStringByRune(env, '\n')
}
/* Splits a request string into it's arguments with the '?' delimiter */
func splitRequestStringArgs(requestStr string) []string {
return splitStringByRune(requestStr, '?')
}
/* Splits a line string into it's arguments with standard space delimiter */
func splitLineStringArgs(requestStr string) []string {
split := Config.CmdParseLineRegex.Split(requestStr, -1)
if split == nil {
return []string{ requestStr }
} else {
return split
}
}
/* Split a string according to a rune, that supports delimiting with '\' */
func splitStringByRune(str string, r rune) []string {
ret := make([]string, 0)
buf := ""
delim := false
for _, c := range str {
switch c {
case r:
if !delim {
ret = append(ret, buf)
buf = ""
} else {
buf += string(c)
delim = false
}
case '\\':
if !delim {
delim = true
} else {
buf += string(c)
delim = false
}
default:
if !delim {
buf += string(c)
} else {
buf += "\\"+string(c)
delim = false
}
}
}
if len(buf) > 0 || len(ret) == 0 {
ret = append(ret, buf)
}
return ret
}

View File

@ -4,6 +4,12 @@ import (
"os"
)
const (
/* Filename constants */
CapsTxtStr = "caps.txt"
RobotsTxtStr = "robots.txt"
)
func cachePolicyFiles(description, admin, geoloc string) {
/* See if caps txt exists, if not generate */
_, err := os.Stat("/caps.txt")
@ -19,7 +25,7 @@ func cachePolicyFiles(description, admin, geoloc string) {
file.LoadContents()
/* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/caps.txt", file)
Config.FileSystem.CacheMap.Put("/"+CapsTxtStr, file)
}
/* See if caps txt exists, if not generate */
@ -36,7 +42,7 @@ func cachePolicyFiles(description, admin, geoloc string) {
file.LoadContents()
/* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/robots.txt", file)
Config.FileSystem.CacheMap.Put("/"+RobotsTxtStr, file)
}
}

View File

@ -5,6 +5,10 @@ import (
"strings"
)
func compileCmdParseRegex() *regexp.Regexp {
return regexp.MustCompile(` `)
}
func compileUserRestrictedFilesRegex(restrictedFiles string) []*regexp.Regexp {
/* Return slice */
restrictedFilesRegex := make([]*regexp.Regexp, 0)

View File

@ -4,6 +4,12 @@ import (
"strings"
)
const (
/* Socket settings */
SocketReadBufSize = 256 /* Supplied selector should be <= this len */
MaxSocketReadChunks = 1
)
type Worker struct {
Conn *GophorConn
}
@ -58,29 +64,17 @@ func (worker *Worker) Serve() {
/* Handle any error */
if gophorErr != nil {
Config.SysLog.Error("", "%s\n", gophorErr.Error())
/* 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)
worker.Send(response)
}
}
}
func (worker *Worker) SendRaw(b []byte) *GophorError {
count, err := worker.Conn.Write(b)
if err != nil {
return &GophorError{ SocketWriteErr, err }
} else if count != len(b) {
return &GophorError{ SocketWriteCountErr, nil }
}
return nil
}
func (worker *Worker) Log(format string, args ...interface{}) {
Config.AccLog.Info("("+worker.Conn.RemoteAddr().String()+") ", format, args...)
}
@ -89,6 +83,16 @@ func (worker *Worker) LogError(format string, args ...interface{}) {
Config.AccLog.Error("("+worker.Conn.RemoteAddr().String()+") ", format, args...)
}
func (worker *Worker) Send(b []byte) *GophorError {
count, err := worker.Conn.Write(b)
if err != nil {
return &GophorError{ SocketWriteErr, err }
} else if count != len(b) {
return &GophorError{ SocketWriteCountErr, nil }
}
return nil
}
func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* According to Gopher spec, only read up to first Tab or Crlf */
dataStr := readUpToFirstTabOrCrlf(data)
@ -99,25 +103,30 @@ func (worker *Worker) RespondGopher(data []byte) *GophorError {
switch len(dataStr) {
case lenBefore-4:
/* Send an HTML redirect to supplied URL */
worker.Log("Redirecting to %s\n", dataStr)
return worker.SendRaw(generateHtmlRedirect(dataStr))
worker.LogError("Redirecting to %s\n", dataStr)
return worker.Send(generateHtmlRedirect(dataStr))
default:
/* Do nothing */
}
/* Get request path from data string */
requestPath := NewSanitizedRequestPath(worker.Conn.Host.RootDir, dataStr)
/* Append lastline */
response, gophorErr := Config.FileSystem.HandleRequest(requestPath, worker.Conn.Host)
/* Parse filesystem request and arg string slice */
requestPath, args, gophorErr := parseFileSystemRequest(worker.Conn.Host.RootDir, dataStr)
if gophorErr != nil {
return gophorErr
}
/* Handle filesystem request */
response, gophorErr := Config.FileSystem.HandleRequest(worker.Conn.Host, requestPath, args)
if gophorErr != nil {
/* Log to system and access logs, then return error */
Config.SysLog.Error("", "Error serving %s: %s\n", dataStr, gophorErr.Error())
worker.LogError("Failed to serve: %s\n", requestPath.AbsolutePath())
return gophorErr
}
worker.Log("Served: %s\n", requestPath.AbsolutePath())
/* Serve response */
return worker.SendRaw(response)
return worker.Send(response)
}
func readUpToFirstTabOrCrlf(data []byte) string {