huge changes

- massively improve request handling -- passes request object between
  functions now to encapsulate all the data needed

- writing is now handled by the passed request object with attached
  writer

- files > max cached file size and fed directly to socket now instead
  of read into buffer first (which would have caused MAJOR memory issues)

- CGI support further fleshed out, and timer started for child processes
  that attempts to kill them if they don't clean up in time

- probably many more fixes i'm forgetting

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
master
kim (grufwub) 4 years ago
parent 4ea4280cd0
commit 3a41efcc5b

@ -22,8 +22,9 @@ type ServerConfig struct {
PageWidth int
/* Regex */
CmdParseLineRegex *regexp.Regexp
RestrictedFiles []*regexp.Regexp
CmdParseLineRegex *regexp.Regexp
RestrictedFiles []*regexp.Regexp
RestrictedCommands []*regexp.Regexp
/* Logging */
SysLog LoggerInterface

@ -7,10 +7,8 @@ import (
type ConnHost struct {
/* Hold host specific details */
Name string
Port string
RootDir string
}
type ConnClient struct {
@ -26,11 +24,13 @@ type GophorListener struct {
Listener net.Listener
Host *ConnHost
RootDir string
}
func BeginGophorListen(bindAddr, hostname, port, rootDir string) (*GophorListener, error) {
gophorListener := new(GophorListener)
gophorListener.Host = &ConnHost{ hostname, port, rootDir }
gophorListener.Host = &ConnHost{ hostname, port }
gophorListener.RootDir = rootDir
var err error
gophorListener.Listener, err = net.Listen("tcp", bindAddr+":"+port)
@ -51,7 +51,8 @@ func (l *GophorListener) Accept() (*GophorConn, error) {
gophorConn.Conn = conn
/* Copy over listener host */
gophorConn.Host = l.Host
gophorConn.Host = l.Host
gophorConn.RootDir = l.RootDir
/* Should always be ok as listener is type TCP (see above) */
addr, _ := conn.RemoteAddr().(*net.TCPAddr)
@ -70,9 +71,10 @@ func (l *GophorListener) Addr() net.Addr {
type GophorConn struct {
/* Simple net.Conn wrapper with virtual host and client info */
Conn net.Conn
Host *ConnHost
Client *ConnClient
Conn net.Conn
Host *ConnHost
Client *ConnClient
RootDir string
}
func (c *GophorConn) Read(b []byte) (int, error) {
@ -86,3 +88,19 @@ func (c *GophorConn) Write(b []byte) (int, error) {
func (c *GophorConn) Close() error {
return c.Conn.Close()
}
func (c *GophorConn) Hostname() string {
return c.Host.Name
}
func (c *GophorConn) HostPort() string {
return c.Host.Port
}
func (c *GophorConn) HostRoot() string {
return c.RootDir
}
func (c *GophorConn) ClientAddr() string {
return c.Client.Ip
}

@ -9,28 +9,29 @@ type ErrorCode int
type ErrorResponseCode int
const (
/* Filesystem */
PathEnumerationErr ErrorCode = iota
IllegalPathErr ErrorCode = iota
FileStatErr ErrorCode = iota
FileOpenErr ErrorCode = iota
FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota
PathEnumerationErr ErrorCode = iota
IllegalPathErr ErrorCode = iota
FileStatErr ErrorCode = iota
FileOpenErr ErrorCode = iota
FileReadErr ErrorCode = iota
FileTypeErr ErrorCode = iota
DirListErr ErrorCode = iota
/* Sockets */
SocketWriteErr ErrorCode = iota
SocketWriteCountErr ErrorCode = iota
RequestWriteErr ErrorCode = iota
RequestWriteCountErr ErrorCode = iota
/* Parsing */
InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr ErrorCode = iota
InvalidGophermapErr ErrorCode = iota
InvalidRequestErr ErrorCode = iota
EmptyItemTypeErr ErrorCode = iota
InvalidGophermapErr ErrorCode = iota
/* Executing */
BufferReadErr ErrorCode = iota
CommandStartErr ErrorCode = iota
CommandExitCodeErr ErrorCode = iota
CgiDisabledErr ErrorCode = iota
BufferReadErr ErrorCode = iota
CommandStartErr ErrorCode = iota
CommandExitCodeErr ErrorCode = iota
CgiDisabledErr ErrorCode = iota
RestrictedCommandErr ErrorCode = iota
/* Error Response Codes */
ErrorResponse200 ErrorResponseCode = iota
@ -71,10 +72,10 @@ func (e *GophorError) Error() string {
case DirListErr:
str = "directory read fail"
case SocketWriteErr:
str = "socket write fail"
case SocketWriteCountErr:
str = "socket write count mismatch"
case RequestWriteErr:
str = "request write fail"
case RequestWriteCountErr:
str = "request write count mismatch"
case InvalidRequestErr:
str = "invalid request data"
@ -91,6 +92,8 @@ func (e *GophorError) Error() string {
str = "command exit code non-zero"
case CgiDisabledErr:
str = "ignoring /cgi-bin request, CGI disabled"
case RestrictedCommandErr:
str = "command use restricted"
default:
str = "Unknown"
@ -123,9 +126,9 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
return ErrorResponse404
/* These are errors _while_ sending, no point trying to send error */
case SocketWriteErr:
case RequestWriteErr:
return NoResponse
case SocketWriteCountErr:
case RequestWriteCountErr:
return NoResponse
case InvalidRequestErr:
@ -143,6 +146,8 @@ func gophorErrorToResponseCode(code ErrorCode) ErrorResponseCode {
return ErrorResponse500
case CgiDisabledErr:
return ErrorResponse404
case RestrictedCommandErr:
return ErrorResponse500
default:
return ErrorResponse503

@ -3,8 +3,8 @@ package main
import (
"os/exec"
"syscall"
"bytes"
"strconv"
"time"
"io"
)
@ -14,8 +14,8 @@ const (
ExecTypeCgi ExecType = iota
ExecTypeRegular ExecType = iota
SafeExecPath = "/usr/bin:/bin"
ReaderBufSize = 1024
SafeExecPath = "/usr/bin:/bin"
MaxExecRunTimeMs = time.Duration(time.Millisecond * 2500)
)
func setupExecEnviron() []string {
@ -40,18 +40,14 @@ func setupInitialCgiEnviron() []string {
}
}
func executeCgi(request *FileSystemRequest) ([]byte, *GophorError) {
func executeCgi(request *Request) *GophorError {
/* Get initial CgiEnv variables */
cgiEnv := Config.CgiEnv
cgiEnv = append(cgiEnv, envKeyValue("SERVER_NAME", request.Host.Name)) /* MUST be set to name of server host client is connecting to */
cgiEnv = append(cgiEnv, envKeyValue("SERVER_PORT", request.Host.Port)) /* MUST be set to the server port that client is connecting to */
cgiEnv = append(cgiEnv, envKeyValue("REMOTE_ADDR", request.Client.Ip)) /* Remote client addr, MUST be set */
/* Fuck it. For now, we don't support PATH_INFO. It's a piece of shit variable */
// cgiEnv = append(cgiEnv, envKeyValue("PATH_INFO", request.Parameters[0])) /* Sub-resource to be fetched by script, derived from path hierarch portion of URI. NOT URL encoded */
/* We store the query string in Parameters[1]. Ensure we git without initial delimiter */
/* We store the query string in Parameters[0]. Ensure we git without initial delimiter */
var queryString string
if len(request.Parameters[0]) > 0 {
queryString = request.Parameters[0][1:]
@ -59,15 +55,16 @@ func executeCgi(request *FileSystemRequest) ([]byte, *GophorError) {
queryString = request.Parameters[0]
}
cgiEnv = append(cgiEnv, envKeyValue("QUERY_STRING", queryString)) /* URL encoded search or parameter string, MUST be set even if empty */
cgiEnv = append(cgiEnv, envKeyValue("PATH_TRANSLATED", request.AbsPath())) /* Take PATH_INFO, parse as local URI and append root dir */
cgiEnv = append(cgiEnv, envKeyValue("SCRIPT_NAME", "/"+request.RelPath())) /* URI path (not URL encoded) which could identify the CGI script (rather than script's output) */
cgiEnv = append(cgiEnv, envKeyValue("SCRIPT_FILENAME", request.AbsPath())) /* Basically SCRIPT_NAME absolute path */
cgiEnv = append(cgiEnv, envKeyValue("SELECTOR", request.SelectorPath()))
cgiEnv = append(cgiEnv, envKeyValue("SCRIPT_FILENAME", request.AbsPath()))
cgiEnv = append(cgiEnv, envKeyValue("DOCUMENT_ROOT", request.RootDir))
cgiEnv = append(cgiEnv, envKeyValue("DOCUMENT_ROOT", request.RootDir()))
cgiEnv = append(cgiEnv, envKeyValue("REQUEST_URI", "/"+request.RelPath()+request.Parameters[0]))
/* Fuck it. For now, we don't support PATH_INFO. It's a piece of shit variable */
// cgiEnv = append(cgiEnv, envKeyValue("PATH_INFO", request.Parameters[0])) /* Sub-resource to be fetched by script, derived from path hierarch portion of URI. NOT URL encoded */
// cgiEnv = append(cgiEnv, envKeyValue("PATH_TRANSLATED", request.AbsPath())) /* Take PATH_INFO, parse as local URI and append root dir */
/* We ignore these due to just CBA and we're not implementing authorization yet */
// cgiEnv = append(cgiEnv, envKeyValue("AUTH_TYPE", "")) /* Any method used my server to authenticate user, MUST be set if auth'd */
// cgiEnv = append(cgiEnv, envKeyValue("CONTENT_TYPE", "")) /* Only a MUST if HTTP content-type set (so never for gopher) */
@ -75,21 +72,21 @@ func executeCgi(request *FileSystemRequest) ([]byte, *GophorError) {
// cgiEnv = append(cgiEnv, envKeyValue("REMOTE_HOST", "")) /* Remote client domain name */
// cgiEnv = append(cgiEnv, envKeyValue("REMOTE_USER", "")) /* Remote user ID, if AUTH_TYPE, MUST be set */
return execute(cgiEnv, request.AbsPath(), nil)
return execute(request.Writer, cgiEnv, request.AbsPath(), nil)
}
func executeFile(request *FileSystemRequest) ([]byte, *GophorError) {
return execute(Config.Env, request.AbsPath(), request.Parameters)
func executeFile(request *Request) *GophorError {
return execute(request.Writer, Config.Env, request.AbsPath(), request.Parameters)
}
func executeCommand(request *FileSystemRequest) ([]byte, *GophorError) {
return execute(Config.Env, request.AbsPath(), request.Parameters)
func executeCommand(request *Request) *GophorError {
if isRestrictedCommand(request.AbsPath()) {
return &GophorError{ RestrictedCommandErr, nil }
}
return execute(request.Writer, Config.Env, request.AbsPath(), request.Parameters)
}
func execute(env []string, path string, args []string) ([]byte, *GophorError) {
/* Create stdout, stderr buffers */
outBuffer := &bytes.Buffer{}
func execute(writer io.Writer, env []string, path string, args []string) *GophorError {
/* Setup command */
var cmd *exec.Cmd
if args != nil {
@ -98,80 +95,66 @@ func execute(env []string, path string, args []string) ([]byte, *GophorError) {
cmd = exec.Command(path)
}
/* Set new proccess group id */
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
/* Setup cmd env */
cmd.Env = env
/* Setup out buffer */
cmd.Stdout = outBuffer
cmd.Stdout = writer
/* Start executing! */
err := cmd.Start()
if err != nil {
return nil, &GophorError{ CommandStartErr, err }
return &GophorError{ CommandStartErr, err }
}
/* Setup timer goroutine to kill cmd after x time */
go func() {
time.Sleep(MaxExecRunTimeMs)
if cmd.ProcessState != nil {
/* We've already finished */
return
}
/* Get process group id */
pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err != nil {
Config.SysLog.Fatal("", "Process unfinished, PGID not found!\n")
}
/* Kill process group! */
err = syscall.Kill(-pgid, syscall.SIGTERM)
if err != nil {
Config.SysLog.Fatal("", "Error stopping process group %d: %s\n", pgid, err.Error())
}
}()
/* Wait for command to finish, get exit code */
err = cmd.Wait()
exitCode := 0
if err != nil {
/* Error, try to get exit code */
exitError, ok := err.(*exec.ExitError)
if ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
} else {
exitCode = 1
}
exitError, _ := err.(*exec.ExitError)
waitStatus := exitError.Sys().(syscall.WaitStatus)
exitCode = waitStatus.ExitStatus()
} 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 */
//errContents, gophorErr := readBuffer(errBuffer)
Config.SysLog.Error("", "Error executing: %s\n", cmd.String())
return nil, &GophorError{ CommandExitCodeErr, err }
return &GophorError{ CommandExitCodeErr, err }
} 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, gophorErr
}
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 nil
}
return contents, nil
}
func envKeyValue(key, value string) string {
return key+"="+value
}

@ -11,8 +11,8 @@ type FileContents interface {
* for holding onto some level of information about the
* contents of a file.
*/
Render(*FileSystemRequest) []byte
Load() *GophorError
Render(*Request) *GophorError
Load() *GophorError
Clear()
}
@ -20,8 +20,8 @@ type GeneratedFileContents struct {
Contents []byte /* Generated file contents as byte slice */
}
func (fc *GeneratedFileContents) Render(request *FileSystemRequest) []byte {
return fc.Contents
func (fc *GeneratedFileContents) Render(request *Request) *GophorError {
return request.Write(fc.Contents)
}
func (fc *GeneratedFileContents) Load() *GophorError {
@ -34,12 +34,12 @@ func (fc *GeneratedFileContents) Clear() {
}
type RegularFileContents struct {
Request *FileSystemRequest /* Stored filesystem request */
Contents []byte /* File contents as byte slice */
Request *Request /* Stored filesystem request */
Contents []byte /* File contents as byte slice */
}
func (fc *RegularFileContents) Render(request *FileSystemRequest) []byte {
return fc.Contents
func (fc *RegularFileContents) Render(request *Request) *GophorError {
return request.Write(fc.Contents)
}
func (fc *RegularFileContents) Load() *GophorError {
@ -54,25 +54,21 @@ func (fc *RegularFileContents) Clear() {
}
type GophermapContents struct {
Request *FileSystemRequest /* Stored filesystem request */
Request *Request /* Stored filesystem request */
Sections []GophermapSection /* Slice to hold differing gophermap sections */
}
func (gc *GophermapContents) Render(request *FileSystemRequest) []byte {
returnContents := make([]byte, 0)
/* Render each of the gophermap sections into byte slices */
func (gc *GophermapContents) Render(request *Request) *GophorError {
/* Render and send each of the gophermap sections */
var gophorErr *GophorError
for _, line := range gc.Sections {
content, gophorErr := line.Render(request)
gophorErr = line.Render(request)
if gophorErr != nil {
content = buildInfoLine(GophermapRenderErrorStr)
Config.SysLog.Error("", "Error executing gophermap contents: %s\n", gophorErr.Error())
}
returnContents = append(returnContents, content...)
}
/* The footer added later contains last line, don't need to worry */
return returnContents
/* End on footer text (including lastline) */
return request.Write(Config.FooterText)
}
func (gc *GophermapContents) Load() *GophorError {
@ -91,31 +87,30 @@ type GophermapSection interface {
* sections and render when necessary
*/
Render(*FileSystemRequest) ([]byte, *GophorError)
Render(*Request) *GophorError
}
type GophermapText struct {
Contents []byte /* Text contents */
}
func (s *GophermapText) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return replaceStrings(string(s.Contents), request.Host), nil
func (s *GophermapText) Render(request *Request) *GophorError {
return request.Write(replaceStrings(string(s.Contents), request.Host))
}
type GophermapDirListing struct {
Request *FileSystemRequest /* Stored filesystem request */
Request *Request /* Stored filesystem request */
Hidden map[string]bool /* Hidden files map parsed from gophermap */
}
func (g *GophermapDirListing) Render(request *FileSystemRequest) ([]byte, *GophorError) {
func (g *GophermapDirListing) Render(request *Request) *GophorError {
/* Create new filesystem request from mixture of stored + supplied */
return listDir(
&FileSystemRequest{
&Request{
request.Host,
request.Client,
g.Request.RootDir,
g.Request.RelPath(),
g.Request.AbsPath(),
request.Writer,
g.Request.Path,
g.Request.Parameters,
},
g.Hidden,
@ -123,31 +118,49 @@ func (g *GophermapDirListing) Render(request *FileSystemRequest) ([]byte, *Gopho
}
type GophermapExecCgi struct {
Request *FileSystemRequest /* Stored file system request */
Request *Request /* Stored file system request */
}
func (g *GophermapExecCgi) Render(request *FileSystemRequest) ([]byte, *GophorError) {
func (g *GophermapExecCgi) Render(request *Request) *GophorError {
/* Create new filesystem request from mixture of stored + supplied */
return executeCgi(g.Request)
return executeCgi(&Request{
request.Host,
request.Client,
request.Writer,
g.Request.Path,
g.Request.Parameters,
})
}
type GophermapExecFile struct {
Request *FileSystemRequest /* Stored file system request */
Request *Request /* Stored file system request */
}
func (g *GophermapExecFile) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return executeCommand(g.Request)
func (g *GophermapExecFile) Render(request *Request) *GophorError {
return executeFile(&Request{
request.Host,
request.Client,
request.Writer,
g.Request.Path,
g.Request.Parameters,
})
}
type GophermapExecCommand struct {
Request *FileSystemRequest
Request *Request
}
func (g *GophermapExecCommand) Render(request *FileSystemRequest) ([]byte, *GophorError) {
return executeCommand(g.Request)
func (g *GophermapExecCommand) Render(request *Request) *GophorError {
return executeCommand(&Request{
request.Host,
request.Client,
request.Writer,
g.Request.Path,
g.Request.Parameters,
})
}
func readGophermap(request *FileSystemRequest) ([]GophermapSection, *GophorError) {
func readGophermap(request *Request) ([]GophermapSection, *GophorError) {
/* Create return slice */
sections := make([]GophermapSection, 0)
@ -189,12 +202,15 @@ func readGophermap(request *FileSystemRequest) ([]GophermapSection, *GophorError
case TypeSubGophermap:
/* Parse new requestPath and parameters (this automatically sanitizes requestPath) */
subRequest := parseLineRequestString(request, line[1:])
subPath, subParameters := parseLineRequestString(request.Path, line[1:])
subRequest := NewRequest(nil, nil, nil, subPath, subParameters)
if !subRequest.HasAbsPathPrefix("/") {
if !subRequest.PathHasAbsPrefix("/") {
if Config.CgiEnabled {
/* Special case here where command must be in path, return GophermapExecCommand */
sections = append(sections, &GophermapExecCommand{ subRequest })
} else {
break
}
} else if subRequest.RelPath() == "" {
/* path cleaning failed */
@ -212,9 +228,9 @@ func readGophermap(request *FileSystemRequest) ([]GophermapSection, *GophorError
}
/* Check if we've been supplied subgophermap or regular file */
if subRequest.HasAbsPathSuffix("/"+GophermapFileStr) {
if subRequest.PathHasAbsSuffix("/"+GophermapFileStr) {
/* If executable, store as GophermapExecutable, else readGophermap() */
if stat.Mode().Perm() & 0100 != 0 && Config.CgiEnabled {
if Config.CgiEnabled && stat.Mode().Perm() & 0100 != 0 {
sections = append(sections, &GophermapExecFile { subRequest })
} else {
/* Treat as any other gophermap! */
@ -225,7 +241,7 @@ func readGophermap(request *FileSystemRequest) ([]GophermapSection, *GophorError
}
} else {
/* If stored in cgi-bin store as GophermapExecutable, else read into GophermapText */
if subRequest.HasRelPathPrefix(CgiBinDirStr) && Config.CgiEnabled {
if Config.CgiEnabled && subRequest.PathHasRelPrefix(CgiBinDirStr) {
sections = append(sections, &GophermapExecCgi{ subRequest })
} else {
fileContents, gophorErr := readIntoGophermap(subRequest.AbsPath())
@ -244,7 +260,8 @@ func readGophermap(request *FileSystemRequest) ([]GophermapSection, *GophorError
case TypeEndBeginList:
/* Create GophermapDirListing object then break out at end of loop */
dirRequest := NewFileSystemRequest(nil, nil, request.RootDir, request.TrimRelPathSuffix(GophermapFileStr), request.Parameters)
dirPath := NewRequestPath(request.RootDir(), request.PathTrimRelSuffix(GophermapFileStr))
dirRequest := NewRequest(nil, nil, nil, dirPath, request.Parameters)
dirListing = &GophermapDirListing{ dirRequest, hidden }
return false

@ -32,7 +32,12 @@ func (fs *FileSystem) Init(size int, fileSizeMax float64) {
fs.CacheFileMax = int64(BytesInMegaByte * fileSizeMax)
}
func (fs *FileSystem) HandleRequest(request *FileSystemRequest) ([]byte, *GophorError) {
func (fs *FileSystem) HandleRequest(request *Request) *GophorError {
/* Check if restricted file */
if isRestrictedFile(request.AbsPath()) {
return &GophorError{ IllegalPathErr, nil }
}
/* Get filesystem stat, check it exists! */
stat, err := os.Stat(request.AbsPath())
if err != nil {
@ -41,16 +46,16 @@ func (fs *FileSystem) HandleRequest(request *FileSystemRequest) ([]byte, *Gophor
file := fs.CacheMap.Get(request.AbsPath())
if file == nil {
fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
return &GophorError{ FileStatErr, err }
}
/* It's there! Get contents, unlock and return */
file.Mutex.RLock()
b := file.Contents(request)
gophorErr := file.WriteContents(request)
file.Mutex.RUnlock()
fs.CacheMutex.RUnlock()
return b, nil
return gophorErr
}
/* Handle file type */
@ -58,46 +63,40 @@ func (fs *FileSystem) HandleRequest(request *FileSystemRequest) ([]byte, *Gophor
/* Directory */
case stat.Mode() & os.ModeDir != 0:
/* Ignore anything under cgi-bin directory */
if request.HasRelPathPrefix(CgiBinDirStr) {
return nil, &GophorError{ IllegalPathErr, nil }
if request.PathHasRelPrefix(CgiBinDirStr) {
return &GophorError{ IllegalPathErr, nil }
}
/* Check Gophermap exists */
gophermapRequest := NewFileSystemRequest(request.Host, request.Client, request.RootDir, request.JoinRelPath(GophermapFileStr), request.Parameters)
stat, err = os.Stat(gophermapRequest.AbsPath())
gophermapPath := NewRequestPath(request.RootDir(), request.PathJoinRel(GophermapFileStr))
stat, err = os.Stat(gophermapPath.Absolute())
var output []byte
var gophorErr *GophorError
if err == nil {
/* Gophermap exists! If executable execute, else serve. */
/* Gophermap exists! If executable and CGI enabled execute, else serve. */
gophermapRequest := NewRequest(request.Host, request.Client, request.Writer, gophermapPath, request.Parameters)
if stat.Mode().Perm() & 0100 != 0 {
output, gophorErr = executeFile(gophermapRequest)
if Config.CgiEnabled {
return executeFile(gophermapRequest)
} else {
return &GophorError{ CgiDisabledErr, nil }
}
} else {
output, gophorErr = fs.FetchFile(gophermapRequest)
return fs.FetchFile(gophermapRequest)
}
} else {
/* No gophermap, serve directory listing */
output, gophorErr = listDir(request, map[string]bool{})
}
if gophorErr != nil {
/* Fail out! */
return nil, gophorErr
return listDir(request, map[string]bool{})
}
/* Append footer text (contains last line) and return */
output = append(output, Config.FooterText...)
return output, nil
/* Regular file */
case stat.Mode() & os.ModeType == 0:
/* If cgi-bin and CGI enabled, return executed contents. Else, fetch */
if request.HasRelPathPrefix(CgiBinDirStr) {
if request.PathHasRelPrefix(CgiBinDirStr) {
if Config.CgiEnabled {
return executeCgi(request)
} else {
return nil, &GophorError{ CgiDisabledErr, nil }
return &GophorError{ CgiDisabledErr, nil }
}
} else {
return fs.FetchFile(request)
@ -105,11 +104,11 @@ func (fs *FileSystem) HandleRequest(request *FileSystemRequest) ([]byte, *Gophor
/* Unsupported type */
default:
return nil, &GophorError{ FileTypeErr, nil }
return &GophorError{ FileTypeErr, nil }
}
}
func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorError) {
func (fs *FileSystem) FetchFile(request *Request) *GophorError {
/* Get cache map read lock then check if file in cache map */
fs.CacheMutex.RLock()
file := fs.CacheMap.Get(request.AbsPath())
@ -125,12 +124,12 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
file.Mutex.Lock()
/* Reload file contents from disk */
gophorErr := file.LoadContents()
gophorErr := file.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock all mutex then return error */
file.Mutex.Unlock()
fs.CacheMutex.RUnlock()
return nil, gophorErr
return gophorErr
}
/* Updated! Swap back file write for read lock */
@ -138,42 +137,55 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
file.Mutex.RLock()
}
} else {
/* Perform filesystem stat ready for checking file size later.
* Doing this now allows us to weed-out non-existent files early
/* Open file here, to check it exists, ready for file stat
* and in case file is too big we pass it as a raw response
*/
stat, err := os.Stat(request.AbsPath())
fd, err := os.Open(request.AbsPath())
if err != nil {
/* Error stat'ing file, unlock read mutex then return error */
fs.CacheMutex.RUnlock()
return nil, &GophorError{ FileStatErr, err }
return &GophorError{ FileOpenErr, err }
}
/* We need a doctor, stat! */
stat, err := fd.Stat()
if err != nil {
/* Error stat'ing file, unlock read mutext then return */
fs.CacheMutex.RUnlock()
return &GophorError{ FileStatErr, err }
}
/* Compare file size (in MB) to CacheFileSizeMax. If larger, just send file raw */
if stat.Size() > fs.CacheFileMax {
/* Unlock the read mutex, we don't need it where we're going... returning, we're returning. */
fs.CacheMutex.RUnlock()
return request.WriteRaw(fd)
}
/* Create new file contents */
var contents FileContents
if request.HasAbsPathSuffix("/"+GophermapFileStr) {
contents = &GophermapContents{ request, nil }
if request.PathHasAbsSuffix("/"+GophermapFileStr) {
contents = &GophermapContents{ request.CachedRequest(), nil }
} else {
contents = &RegularFileContents{ request, nil }
contents = &RegularFileContents{ request.CachedRequest(), nil }
}
/* Compare file size (in MB) to CacheFileSizeMax. If larger, just send file raw */
if stat.Size() > fs.CacheFileMax {
/* Unlock the read mutex, we don't need it where we're going... returning, we're returning. */
fs.CacheMutex.RUnlock()
return contents.Render(request)
}
/* Create new file wrapper around contents */
file = &File{ contents, sync.RWMutex{}, true, time.Now().UnixNano() }
/* File isn't in cache yet so no need to get file lock mutex */
gophorErr := file.LoadContents()
gophorErr := file.CacheContents()
if gophorErr != nil {
/* Error loading contents, unlock read mutex then return error */
fs.CacheMutex.RUnlock()
return nil, gophorErr
}
/* Compare file size (in MB) to CacheFileSizeMax, if larger just get file
* contents, unlock all mutex and don't bother caching.
*/
if stat.Size() > fs.CacheFileMax {
b := file.Contents(request)
fs.CacheMutex.RUnlock()
return b, nil
return gophorErr
}
/* File not in cache -- Swap cache map read for write lock. */
@ -192,13 +204,13 @@ func (fs *FileSystem) FetchFile(request *FileSystemRequest) ([]byte, *GophorErro
}
/* Read file contents into new variable for return, then unlock file read lock */
b := file.Contents(request)
gophorErr := file.WriteContents(request)
file.Mutex.RUnlock()
/* Finally we can unlock the cache map read lock, we are done :) */
fs.CacheMutex.RUnlock()
return b, nil
return gophorErr
}
type File struct {
@ -213,11 +225,11 @@ type File struct {
LastRefresh int64
}
func (f *File) Contents(request *FileSystemRequest) []byte {
func (f *File) WriteContents(request *Request) *GophorError {
return f.Content.Render(request)
}
func (f *File) LoadContents() *GophorError {
func (f *File) CacheContents() *GophorError {
/* Clear current file contents */
f.Content.Clear()

@ -111,19 +111,19 @@ func unixLineEndSplitter(data []byte, atEOF bool) (advance int, token []byte, er
}
/* List the files in a directory, hiding those requested */
func listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *GophorError) {
func listDir(request *Request, hidden map[string]bool) *GophorError {
/* Open directory file descriptor */
fd, err := os.Open(request.AbsPath())
if err != nil {
Config.SysLog.Error("", "failed to open %s: %s\n", request.AbsPath(), err.Error())
return nil, &GophorError{ FileOpenErr, err }
return &GophorError{ FileOpenErr, err }
}
/* Read files in directory */
files, err := fd.Readdir(-1)
if err != nil {
Config.SysLog.Error("", "failed to enumerate dir %s: %s\n", request.AbsPath(), err.Error())
return nil, &GophorError{ DirListErr, err }
return &GophorError{ DirListErr, err }
}
/* Sort the files by name */
@ -137,14 +137,12 @@ func listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *Gopho
dirContents = append(dirContents, buildInfoLine("")...)
/* Add a 'back' entry. GoLang Readdir() seems to miss this */
dirContents = append(dirContents, buildLine(TypeDirectory, "..", request.JoinRelPath(".."), request.Host.Name, request.Host.Port)...)
dirContents = append(dirContents, buildLine(TypeDirectory, "..", request.PathJoinSelector(".."), request.Host.Name, request.Host.Port)...)
/* Walk through files :D */
for _, file := range files {
/* If regex match in restricted files || requested hidden */
if isRestrictedFile(file.Name()) {
continue
} else if _, ok := hidden[file.Name()]; ok {
if _, ok := hidden[file.Name()]; ok {
continue
}
@ -152,12 +150,12 @@ func listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *Gopho
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory -- create directory listing */
itemPath := request.JoinSelectorPath(file.Name())
itemPath := request.PathJoinSelector(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.JoinSelectorPath(file.Name())
itemPath := request.PathJoinSelector(file.Name())
itemType := getItemType(itemPath)
dirContents = append(dirContents, buildLine(itemType, file.Name(), itemPath, request.Host.Name, request.Host.Port)...)
@ -166,7 +164,10 @@ func listDir(request *FileSystemRequest, hidden map[string]bool) ([]byte, *Gopho
}
}
return dirContents, nil
/* Append the footer (including lastline) */
dirContents = append(dirContents, Config.FooterText...)
return request.Write(dirContents)
}
/* Took a leaf out of go-gopher's book here. */

@ -10,7 +10,7 @@ import (
)
const (
GophorVersion = "0.7-beta-PR1"
GophorVersion = "0.7-beta-PR1"
)
var (
@ -23,7 +23,7 @@ func main() {
/* Handle signals so we can _actually_ shutdowm */
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
/* Start accepting connections on any supplied listeners */
for _, l := range listeners {
@ -39,7 +39,8 @@ func main() {
/* Run this in it's own goroutine so we can go straight back to accepting */
go func() {
NewWorker(newConn).Serve()
worker := &Worker{ newConn }
worker.Serve()
}()
}
}()
@ -55,38 +56,41 @@ func setupServer() []*GophorListener {
/* First we setup all the flags and parse them... */
/* Base server settings */
serverRoot := flag.String("root-dir", "/var/gopher", "Change server root directory.")
serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
serverPort := flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).")
serverBindAddr := flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
serverRoot := flag.String("root-dir", "/var/gopher", "Change server root directory.")
serverHostname := flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).")
serverPort := flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).")
serverBindAddr := flag.String("bind-addr", "127.0.0.1", "Change server socket bind address")
/* User supplied caps.txt information */
serverDescription := flag.String("description", "Gophor: a Gopher server in GoLang", "Change server description in generated caps.txt.")
serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.")
serverDescription := flag.String("description", "Gophor, a Gopher server in Go.", "Change server description in generated caps.txt.")
serverAdmin := flag.String("admin-email", "", "Change admin email in generated caps.txt.")
serverGeoloc := flag.String("geoloc", "", "Change server gelocation string in generated caps.txt.")
/* Content settings */
footerText := flag.String("footer", "", "Change gophermap footer text (Unix new-line separated lines).")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
footerText := flag.String("footer", " Gophor, a Gopher server in Go.", "Change gophermap footer text (Unix new-line separated lines).")
footerSeparator := flag.Bool("no-footer-separator", false, "Disable footer line separator.")
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting files from showing in directory listings.")
disableCgi := flag.Bool("disable-cgi", false, "Disable CGI and all executable support.")
pageWidth := flag.Int("page-width", 80, "Change page width used when formatting output.")
disableCgi := flag.Bool("disable-cgi", false, "Disable CGI and all executable support.")
/* Regex */
restrictedFiles := flag.String("restrict-files", "", "New-line separated list of regex statements restricting accessible files.")
restrictedCommands := flag.String("restrict-commands", "", "New-line separated list of regex statements restricting accessible commands.")
/* Logging settings */
systemLogPath := flag.String("system-log", "", "Change server system log file (blank outputs to stderr).")
accessLogPath := flag.String("access-log", "", "Change server access log file (blank outputs to stderr).")
logOutput := flag.String("log-output", "stderr", "Change server log file handling (disable|stderr|file)")
logOpts := flag.String("log-opts", "timestamp,ip", "Comma-separated list of log options (timestamp|ip)")
systemLogPath := flag.String("system-log", "", "Change server system log file (blank outputs to stderr).")
accessLogPath := flag.String("access-log", "", "Change server access log file (blank outputs to stderr).")
logOutput := flag.String("log-output", "stderr", "Change server log file handling (disable|stderr|file)")
logOpts := flag.String("log-opts", "timestamp,ip", "Comma-separated list of log options (timestamp|ip)")
/* Cache settings */
cacheCheckFreq := flag.String("cache-check", "60s", "Change file cache freshness check frequency.")
cacheSize := flag.Int("cache-size", 50, "Change file cache size, measured in file count.")
cacheFileSizeMax := flag.Float64("cache-file-max", 0.5, "Change maximum file size to be cached (in megabytes).")
cacheDisabled := flag.Bool("disable-cache", false, "Disable file caching.")
fileMonitorFreq := flag.Duration("file-monitor-freq", time.Second*60, "Change file monitor frequency.")
cacheSize := flag.Int("cache-size", 50, "Change file cache size, measured in file count.")
cacheFileSizeMax := flag.Float64("cache-file-max", 0.5, "Change maximum file size to be cached (in megabytes).")
cacheDisabled := flag.Bool("disable-cache", false, "Disable file caching.")
/* Version string */
version := flag.Bool("version", false, "Print version information.")
version := flag.Bool("version", false, "Print version information.")
/* Parse parse parse!! */
flag.Parse()
@ -96,8 +100,7 @@ func setupServer() []*GophorListener {
/* Setup the server configuration instance and enter as much as we can right now */
Config = new(ServerConfig)
Config.PageWidth = *pageWidth
Config.CgiEnabled = !*disableCgi
Config.PageWidth = *pageWidth
/* Have to be set AFTER page width variable set */
Config.FooterText = formatGophermapFooter(*footerText, !*footerSeparator)
@ -105,6 +108,15 @@ func setupServer() []*GophorListener {
/* Setup Gophor logging system */
Config.SysLog, Config.AccLog = setupLoggers(*logOutput, *logOpts, *systemLogPath, *accessLogPath)
/* Set CGI support status */
if *disableCgi {
Config.SysLog.Info("", "CGI support disabled")
Config.CgiEnabled = false
} else {
Config.SysLog.Info("", "CGI support enabled")
Config.CgiEnabled = true
}
/* If running as root, get ready to drop privileges */
if syscall.Getuid() == 0 || syscall.Getgid() == 0 {
Config.SysLog.Fatal("", "Gophor does not support running as root!\n")
@ -134,41 +146,34 @@ func setupServer() []*GophorListener {
Config.SysLog.Fatal("", "No valid port to listen on\n")
}
/* Compile CmdParse regular expression */
Config.CmdParseLineRegex = compileCmdParseRegex()
/* Compile user restricted files regex */
Config.RestrictedFiles = compileUserRestrictedFilesRegex(*restrictedFiles)
/* Compile regex statements */
Config.CmdParseLineRegex = compileCmdParseRegex()
Config.RestrictedFiles = compileUserRestrictedFilesRegex(*restrictedFiles)
Config.RestrictedCommands = compileUserRestrictedCommandsRegex(*restrictedCommands)
/* Setup file cache */
Config.FileSystem = new(FileSystem)
/* Check if cache requested disabled */
if !*cacheDisabled {
/* Parse suppled cache check frequency time */
fileMonitorSleepTime, err := time.ParseDuration(*cacheCheckFreq)
if err != nil {
Config.SysLog.Fatal("", "Error parsing supplied cache check frequency %s: %s\n", *cacheCheckFreq, err)
}
/* Init file cache */
Config.FileSystem.Init(*cacheSize, *cacheFileSizeMax)
/* Before file monitor or any kind of new goroutines started,
* check if we need to cache generated policy files
*/
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc)
cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
/* Start file cache freshness checker */
go startFileMonitor(fileMonitorSleepTime)
Config.SysLog.Info("", "File caching enabled with: maxcount=%d maxsize=%.3fMB checkfreq=%s\n", *cacheSize, *cacheFileSizeMax, fileMonitorSleepTime)
go startFileMonitor(*fileMonitorFreq)
Config.SysLog.Info("", "File caching enabled with: maxcount=%d maxsize=%.3fMB checkfreq=%s\n", *cacheSize, *cacheFileSizeMax, *fileMonitorFreq)
} else {
/* File caching disabled, init with zero max size so nothing gets cached */
Config.FileSystem.Init(2, 0)
Config.SysLog.Info("", "File caching disabled\n")
/* Safe to cache policy files now */
cachePolicyFiles(*serverDescription, *serverAdmin, *serverGeoloc)
cachePolicyFiles(*serverRoot, *serverDescription, *serverAdmin, *serverGeoloc)
}
/* Return the created listeners slice :) */

@ -63,24 +63,24 @@ func parseLineType(line string) ItemType {
}
/* Parses a line in a gophermap into a filesystem request path and a string slice of arguments */
func parseLineRequestString(request *FileSystemRequest, lineStr string) (*FileSystemRequest) {
func parseLineRequestString(requestPath *RequestPath, lineStr string) (*RequestPath, []string) {
if strings.HasPrefix(lineStr, "/") {
/* We are dealing with a file input of some kind. Figure out if CGI-bin */
if strings.HasPrefix(lineStr[1:], CgiBinDirStr) {
/* CGI-bind script, parse requestPath and parameters as standard URL encoding */
requestPath, parameters := parseRequestString(lineStr)
return NewFileSystemRequest(nil, nil, request.RootDir, requestPath, parameters)
relPath, parameters := parseRequestString(lineStr)
return NewRequestPath(requestPath.RootDir(), relPath), parameters
} else {
/* Regular file, no more parsing needing */
return NewFileSystemRequest(nil, nil, request.RootDir, lineStr[1:], request.Parameters)
return NewRequestPath(requestPath.RootDir(), lineStr[1:]), []string{}
}
} else {
/* We have been passed a command string */
args := splitCommandString(lineStr)
if len(args) > 1 {
return NewFileSystemRequest(nil, nil, "", args[0], args[1:])
return NewRequestPath("", args[0]), args[1:]
} else {
return NewFileSystemRequest(nil, nil, "", args[0], []string{})
return NewRequestPath("", args[0]), []string{}
}
}
}

@ -2,6 +2,7 @@ package main
import (
"os"
"path"
"sync"
)
@ -11,9 +12,9 @@ const (
RobotsTxtStr = "robots.txt"
)
func cachePolicyFiles(description, admin, geoloc string) {
func cachePolicyFiles(rootDir, description, admin, geoloc string) {
/* See if caps txt exists, if not generate */
_, err := os.Stat("/caps.txt")
_, err := os.Stat(path.Join(rootDir, CapsTxtStr))
if err != nil {
/* We need to generate the caps txt and manually load into cache */
content := generateCapsTxt(description, admin, geoloc)
@ -23,14 +24,15 @@ func cachePolicyFiles(description, admin, geoloc string) {
file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */
file.LoadContents()
file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/"+CapsTxtStr, file)
Config.FileSystem.CacheMap.Put(rootDir+"/"+CapsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+CapsTxtStr)
}
/* See if caps txt exists, if not generate */
_, err = os.Stat("/robots.txt")
_, err = os.Stat(rootDir+"/"+RobotsTxtStr)
if err != nil {
/* We need to generate the caps txt and manually load into cache */
content := generateRobotsTxt()
@ -40,10 +42,11 @@ func cachePolicyFiles(description, admin, geoloc string) {
file := &File{ fileContents, sync.RWMutex{}, true, 0 }
/* Trigger a load contents just to set it as fresh etc */
file.LoadContents()
file.CacheContents()
/* No need to worry about mutexes here, no other goroutines running yet */
Config.FileSystem.CacheMap.Put("/"+RobotsTxtStr, file)
Config.FileSystem.CacheMap.Put(rootDir+"/"+RobotsTxtStr, file)
Config.SysLog.Info("", "Generated policy file: %s\n", rootDir+"/"+RobotsTxtStr)
}
}

@ -15,6 +15,9 @@ func compileUserRestrictedFilesRegex(restrictedFiles string) []*regexp.Regexp {
/* Split the user supplied RestrictedFiles string by new-line */
for _, expr := range strings.Split(restrictedFiles, "\n") {
if len(expr) == 0 {
continue
}
regex, err := regexp.Compile(expr)
if err != nil {
Config.SysLog.Fatal("Failed compiling user restricted files regex: %s\n", expr)
@ -25,6 +28,26 @@ func compileUserRestrictedFilesRegex(restrictedFiles string) []*regexp.Regexp {
return restrictedFilesRegex
}
func compileUserRestrictedCommandsRegex(restrictedCommands string) []*regexp.Regexp {
/* Return slice */
restrictedCommandsRegex := make([]*regexp.Regexp, 0)
/* Split the user supplied RestrictedFiles string by new-line */
for _, expr := range strings.Split(restrictedCommands, "\n") {
if len(expr) == 0 {
continue
}
regex, err := regexp.Compile(expr)
if err != nil {
Config.SysLog.Fatal("Failed compiling user restricted commands regex: %s\n", expr)
}
restrictedCommandsRegex = append(restrictedCommandsRegex, regex)
}
return restrictedCommandsRegex
}
/* Iterate through restricted file expressions, check if file _is_ restricted */
func isRestrictedFile(name string) bool {
for _, regex := range Config.RestrictedFiles {
@ -34,3 +57,12 @@ func isRestrictedFile(name string) bool {
}
return false
}
func isRestrictedCommand(name string) bool {
for _, regex := range Config.RestrictedCommands {
if regex.MatchString(name) {
return true
}
}
return false
}

@ -1,136 +1,184 @@
package main
import (
"io"
"path"
"strings"
)
/* TODO: having 2 separate rootdir string values in Host and RootDir
* doesn't sit right with me. It cleans up code a lot for now
* but could get confusing. Figure out a more elegant way of
* structuring the filesystem request that gets passed around.
*/
type RequestPath struct {
/* Path structure to allow hosts at
* different roots while maintaining relative
* and absolute path names for returned values
* and filesystem reading
*/
Root string
Rel string
Abs string
}
func NewRequestPath(rootDir, relPath string) *RequestPath {
return &RequestPath{ rootDir, relPath, path.Join(rootDir, strings.TrimSuffix(relPath, "/")) }
}
func (rp *RequestPath) RootDir() string {
return rp.Root
}
func (rp *RequestPath) Relative() string {
return rp.Rel
}
type FileSystemRequest struct {
/* A file system request with any possible required
* data required. Either handled through FileSystem or to
* direct function like listDir()
func (rp *RequestPath) Absolute() string {
return rp.Abs
}
func (rp *RequestPath) Selector() string {
if rp.Rel == "." {
return "/"
} else {
return "/"+rp.Rel
}
}
type Request struct {
/* A gophor request containing any data necessary.
* Either handled through FileSystem or to direct function like listDir().
*/
/* Virtual host and client information */
/* Can be nil */
Host *ConnHost
Client *ConnClient
/* File path information */
RootDir string
Rel string
Abs string
/* Other parameters */
/* MUST be set */
Writer io.Writer
Path *RequestPath
Parameters []string /* CGI-bin params will be 1 length slice, shell commands populate >=1 */
}
func NewSanitizedFileSystemRequest(host *ConnHost, client *ConnClient, request string) *FileSystemRequest {
func NewSanitizedRequest(conn *GophorConn, requestStr string) *Request {
/* Split dataStr into request path and parameter string (if pressent) */
requestPath, parameters := parseRequestString(request)
requestPath = sanitizeRequestPath(host.RootDir, requestPath)
return NewFileSystemRequest(host, client, host.RootDir, requestPath, parameters)
relPath, parameters := parseRequestString(requestStr)
relPath = sanitizeRelativePath(conn.HostRoot(), relPath)
return NewRequest(conn.Host, conn.Client, conn.Conn, NewRequestPath(conn.HostRoot(), relPath), parameters)
}
func NewFileSystemRequest(host *ConnHost, client *ConnClient, rootDir, requestPath string, parameters []string) *FileSystemRequest {
return &FileSystemRequest{
func NewRequest(host *ConnHost, client *ConnClient, writer io.Writer, path *RequestPath, parameters []string) *Request {
return &Request{
host,
client,
rootDir,
requestPath,
path.Join(rootDir, requestPath),
writer,
path,
parameters,
}
}
func (r *FileSystemRequest) SelectorPath() string {
if r.Rel == "." {
return "/"
} else {
return "/"+r.Rel
}
func (r *Request) AccessLogInfo(format string, args ...interface{}) {
/* You HAVE to be sure that r.Conn is NOT nil before calling this */
Config.AccLog.Info("("+r.Client.Ip+") ", format, args...)
}
func (r *FileSystemRequest) AbsPath() string {
return r.Abs
func (r *Request) AccessLogError(format string, args ...interface{}) {
/* You HAVE to be sure that r.Conn is NOT nil before calling this */
Config.AccLog.Error("("+r.Client.Ip+") ", format, args...)
}
func (r *FileSystemRequest) RelPath() string {
return r.Rel
func (r *Request) WriteRaw(reader io.Reader) *GophorError {
/* You HAVE to be sure that r.Conn is NOT nil before calling this */
_, err := io.Copy(r.Writer, reader)
if err != nil {
return &GophorError{ RequestWriteErr, err }
} else {
return nil
}
}
func (r *FileSystemRequest) JoinSelectorPath(extPath string) string {
if r.Rel == "." {
return path.Join("/", extPath)
func (r *Request) Write(data []byte) *GophorError {
count, err := r.Writer.Write(data)
if err != nil {
return &GophorError{ RequestWriteErr, err }
} else if count != len(data) {
return &GophorError{ RequestWriteCountErr, nil }
} else {
return "/"+path.Join(r.Rel, extPath)
return nil
}
}
func (r *FileSystemRequest) JoinAbsPath(extPath string) string {
func (r *Request) RootDir() string {
return r.Path.RootDir()
}
func (r *Request) AbsPath() string {
return r.Path.Absolute()
}
func (r *Request) RelPath() string {
return r.Path.Relative()
}
func (r *Request) SelectorPath() string {
return r.Path.Selector()
}
func (r *Request) PathJoinSelector(extPath string) string {
return path.Join(r.SelectorPath(), extPath)
}
func (r *Request) PathJoinAbs(extPath string) string {
return path.Join(r.AbsPath(), extPath)
}
func (r *FileSystemRequest) JoinRelPath(extPath string) string {
func (r *Request) PathJoinRel(extPath string) string {
return path.Join(r.RelPath(), extPath)
}
func (r *FileSystemRequest) HasAbsPathPrefix(prefix string) bool {
func (r *Request) PathHasAbsPrefix(prefix string) bool {
return strings.HasPrefix(r.AbsPath(), prefix)
}
func (r *FileSystemRequest) HasRelPathPrefix(prefix string) bool {
func (r *Request) PathHasRelPrefix(prefix string) bool {
return strings.HasPrefix(r.RelPath(), prefix)
}
func (r *FileSystemRequest) HasRelPathSuffix(suffix string) bool {
func (r *Request) PathHasRelSuffix(suffix string) bool {
return strings.HasSuffix(r.RelPath(), suffix)
}
func (r *FileSystemRequest) HasAbsPathSuffix(suffix string) bool {
func (r *Request) PathHasAbsSuffix(suffix string) bool {
return strings.HasSuffix(r.AbsPath(), suffix)
}
func (r *FileSystemRequest) TrimRelPathSuffix(suffix string) string {
func (r *Request) PathTrimRelSuffix(suffix string) string {
return strings.TrimSuffix(strings.TrimSuffix(r.RelPath(), suffix), "/")
}
func (r *FileSystemRequest) TrimAbsPathSuffix(suffix string) string {
func (r *Request) PathTrimAbsSuffix(suffix string) string {
return strings.TrimSuffix(strings.TrimSuffix(r.AbsPath(), suffix), "/")
}
func (r *FileSystemRequest) JoinPathFromRoot(extPath string) string {
return path.Join(r.RootDir, extPath)
}
func (r *FileSystemRequest) NewStoredRequestAtRoot(relPath string, parameters []string) *FileSystemRequest {
/* DANGER THIS DOES NOT CHECK FOR BACK-DIR TRAVERSALS */
return NewFileSystemRequest(nil, nil, r.RootDir, relPath, parameters)
func (r *Request) PathJoinRootDir(extPath string) string {
return path.Join(r.Path.RootDir(), extPath)
}
func (r *FileSystemRequest) NewStoredRequest() *FileSystemRequest {
return NewFileSystemRequest(nil, nil, r.RootDir, r.RelPath(), r.Parameters)
func (r *Request) CachedRequest() *Request {
return NewRequest(nil, nil, nil, r.Path, r.Parameters)
}
/* Sanitize a request path string */
func sanitizeRequestPath(rootDir, requestPath string) string {
func sanitizeRelativePath(rootDir, relPath string) string {
/* Start with a clean :) */
requestPath = path.Clean(requestPath)
relPath = path.Clean(relPath)
if path.IsAbs(requestPath) {
if path.IsAbs(relPath) {
/* Is absolute. Try trimming root and leading '/' */
requestPath = strings.TrimPrefix(strings.TrimPrefix(requestPath, rootDir), "/")
relPath = strings.TrimPrefix(strings.TrimPrefix(relPath, rootDir), "/")
} else {
/* Is relative. If back dir traversal, give them root */
if strings.HasPrefix(requestPath, "..") {
requestPath = ""
if strings.HasPrefix(relPath, "..") {
relPath = ""
}
}
return requestPath
return relPath
}

@ -1,8 +1,8 @@
package main
import (
"strings"
"io"
"strings"
)
const (
@ -15,10 +15,6 @@ type Worker struct {
Conn *GophorConn
}
func NewWorker(conn *GophorConn) *Worker {
return &Worker{ conn }
}
func (worker *Worker) Serve() {
defer func() {
/* Close-up shop */
@ -30,9 +26,10 @@ func (worker *Worker) Serve() {
/* Read buffer + final result */
buf := make([]byte, SocketReadBufSize)
received := make([]byte, 0)
received := ""
iter := 0
endReached := false
for {
/* Buffered read from listener */
count, err = worker.Conn.Read(buf)
@ -45,10 +42,22 @@ func (worker *Worker) Serve() {
return
}
/* Only copy non-null bytes */
received = append(received, buf[:count]...)
if count < SocketReadBufSize {
/* Reached EOF */
/* Copy buffer into received string, stop at first tap or CrLf */
for i := 0; i < count; i += 1 {
if buf[i] == Tab[0] {
endReached = true
break
} else if buf[i] == DOSLineEnd[0] {
if count > i+1 && buf[i+1] == DOSLineEnd[1] {
endReached = true
break
}
}
received += string(buf[i])
}
/* Reached end of request */
if endReached || count < SocketReadBufSize {
break
}
@ -62,81 +71,42 @@ func (worker *Worker) Serve() {
iter += 1
}
/* Handle URL request if presented */
lenBefore := len(received)
received = strings.TrimPrefix(received, "URL:")
switch len(received) {
case lenBefore-4:
/* Send an HTML redirect to supplied URL */
Config.AccLog.Info("("+worker.Conn.ClientAddr()+") ", "Redirecting to %s\n", received)
worker.Conn.Write(generateHtmlRedirect(received))
return
default:
/* Do nothing */
}
/* Create new request from dataStr */
request := NewSanitizedRequest(worker.Conn, received)
/* Handle request */
gophorErr := worker.RespondGopher(received)
gophorErr := Config.FileSystem.HandleRequest(request)
/* Handle any error */
if gophorErr != nil {
/* Log serve failure to access, error to system */
Config.SysLog.Error("", 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.Send(response)
request.Write(response)
}
}
}
func (worker *Worker) Log(format string, args ...interface{}) {
Config.AccLog.Info("("+worker.Conn.Client.Ip+") ", format, args...)
}
func (worker *Worker) LogError(format string, args ...interface{}) {
Config.AccLog.Error("("+worker.Conn.Client.Ip+") ", 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 }
request.AccessLogError("Failed to serve: %s\n", request.AbsPath())
} else {
/* Log served */
request.AccessLogInfo("Served: %s\n", request.AbsPath())
}
return nil
}
func (worker *Worker) RespondGopher(data []byte) *GophorError {
/* Only read up to first tab or cr-lf */
dataStr := ""
dataLen := len(data)
for i := 0; i < dataLen; i += 1 {
if data[i] == Tab[0] {
break
} else if data[i] == DOSLineEnd[0] {
if i == dataLen-1 || data[i+1] == DOSLineEnd[1] {
break
} else {
dataStr += string(data[i])
}
} else {
dataStr += string(data[i])
}
}
/* Handle URL request if presented */
lenBefore := len(dataStr)
dataStr = strings.TrimPrefix(dataStr, "URL:")
switch len(dataStr) {
case lenBefore-4:
/* Send an HTML redirect to supplied URL */
worker.LogError("Redirecting to %s\n", dataStr)
return worker.Send(generateHtmlRedirect(dataStr))
default:
/* Do nothing */
}
/* Create new filesystem request */
request := NewSanitizedFileSystemRequest(worker.Conn.Host, worker.Conn.Client, dataStr)
/* Handle filesystem request */
response, gophorErr := Config.FileSystem.HandleRequest(request)
if gophorErr != nil {
/* Log to system and access logs, then return error */
return gophorErr
}
worker.Log("Served: %s\n", request.AbsPath())
/* Serve response */
return worker.Send(response)
}

Loading…
Cancel
Save