inline shell commands, executable gophermaps, initial cgi-bin support
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
This commit is contained in:
parent
01eb36a814
commit
eac520031a
18
config.go
18
config.go
@ -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
|
||||
}
|
||||
|
97
constants.go
97
constants.go
@ -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] */
|
||||
)
|
21
error.go
21
error.go
@ -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
104
exec.go
Normal 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
|
||||
}
|
@ -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
|
||||
},
|
||||
)
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
127
format.go
@ -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
119
gophor.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
11
logger.go
11
logger.go
@ -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
176
parse.go
Normal 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
|
||||
}
|
10
policy.go
10
policy.go
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
4
regex.go
4
regex.go
@ -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)
|
||||
|
51
worker.go
51
worker.go
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user