Compare commits

...

4 Commits

Author SHA1 Message Date
kim (grufwub) 1dce8a6600 update errors library in module files
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
3 years ago
kim (grufwub) 1598f101f1 use updated errors library
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
3 years ago
kim (grufwub) 0ecc21c1de Update errors library
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
3 years ago
kim (grufwub) d84739d018 Fix core.EscapePath() reserved chars list, add test for EscapePath, add benchmarks core/url path escaping
Signed-off-by: kim (grufwub) <grufwub@gmail.com>
3 years ago

@ -38,11 +38,11 @@ func generateCGIEnv(client *Client, request *Request, pathInfo string) []string
}
// TryExecuteCGIScript attempts to execute supplied CGI script, finding shortest valid path and setting PATH_INFO accordingly
func TryExecuteCGIScript(client *Client, request *Request) errors.Error {
func TryExecuteCGIScript(client *Client, request *Request) error {
// Get relative path with CGI dir stripped
partial := request.Path().Relative()[len(cgiPath.Relative()):]
if len(partial) == 0 {
return ErrRestrictedPath
return ErrRestrictedPath.Extendf("%s is CGI dir", request.Path().Selector())
}
partial = partial[1:]
@ -56,8 +56,11 @@ func TryExecuteCGIScript(client *Client, request *Request) errors.Error {
// original request
if next == -1 {
stat, err := StatFile(request.Path())
if err != nil || !stat.Mode().IsRegular() {
return ErrFileStat
switch {
case err != nil:
return err.(errors.Error).Extend("CGI error")
case !stat.Mode().IsRegular():
return ErrFileType.Extendf("%s CGI error", request.Path().Absolute())
}
return ExecuteCGIScript(client, request, "")
}
@ -85,11 +88,11 @@ func TryExecuteCGIScript(client *Client, request *Request) errors.Error {
}
// No CGI script was found, return not-found error
return ErrFileStat
return ErrFileStat.Extendf("%s CGI error", request.Path().Absolute())
}
// ExecuteCGIScript executes a CGI script, responding with stdout to client
func ExecuteCGIScript(client *Client, request *Request, pathInfo string) errors.Error {
func ExecuteCGIScript(client *Client, request *Request, pathInfo string) error {
// Create cmd object
cmd := exec.Command(request.Path().Absolute())
@ -106,7 +109,7 @@ func ExecuteCGIScript(client *Client, request *Request, pathInfo string) errors.
// Start executing
err := cmd.Start()
if err != nil {
return ErrCGIStart.Wrap(err)
return errors.With(err).WrapWithin(ErrCGIStart).Extend(request.Path().Absolute())
}
// NOTE: we don't set a max CGI script run time anymore,
@ -126,8 +129,7 @@ func ExecuteCGIScript(client *Client, request *Request, pathInfo string) errors.
}
// Log and return
SystemLog.Errorf(cgiExecuteErrStr, request.Path().Absolute(), exitCode)
return ErrCGIExitCode
return ErrCGIExitCode.Extendf("%s: %d", request.Path().Absolute(), exitCode)
}
// Exit fine!

@ -12,7 +12,6 @@ import (
"github.com/grufwub/go-bufpools"
"github.com/grufwub/go-config"
"github.com/grufwub/go-errors"
"github.com/grufwub/go-filecache"
log "github.com/grufwub/go-logger"
)
@ -23,7 +22,7 @@ func usage(code int) {
}
// ParseConfigAndSetup parses necessary core server config from file (and any others defined), and sets up the core ready for Start() to be called
func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newListener func() (*Listener, errors.Error), fileContent func(*Path) FileContent, dirHandler func(*Client, *os.File, *Path) errors.Error, largeHandler func(*Client, *os.File, *Path) errors.Error, appendCgi func(*Client, *Request, []string) []string) {
func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newListener func() (*Listener, error), fileContent func(*Path) FileContent, dirHandler func(*Client, *os.File, *Path) error, largeHandler func(*Client, *os.File, *Path) error, appendCgi func(*Client, *Request, []string) []string) {
// Default configuration file location
configFile := "/etc/gophi." + proto + ".conf"
@ -36,7 +35,7 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
}
configFile = os.Args[2]
case "-v", "--version":
fmt.Println("Gophi (" + proto + ") " + Version)
fmt.Printf("Gophi (%s) %s\n", proto, Version)
os.Exit(0)
default:
usage(1)
@ -95,14 +94,14 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
// Check valid values for BindAddr and Hostname
if Hostname == "" {
if Bind == "" {
SystemLog.Fatal(hostnameBindEmptyStr)
SystemLog.Fatal("At least one of 'hostname' or 'listen' must be non-empty!")
}
Hostname = Bind
}
// Check valid root (i.e. not empty!)
if Root == "" {
SystemLog.Fatal(rootDirEmptyErrStr)
SystemLog.Fatal("No server root directory supplied!")
}
// Set port info
@ -113,10 +112,10 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
// Setup listener BEFORE entering chroot
// in case TLS cert+key needs to be read
var err errors.Error
var err error
serverListener, err = newListener()
if err != nil {
SystemLog.Fatalf(listenerBeginFailStr, protocol, Hostname, Port, Bind, Port, err.Error())
SystemLog.Fatalf("Failed to start listener on %s://%s:%s (%s:%s) - %s", protocol, Hostname, Port, Bind, Port, err.Error())
}
// Setup the sync pools
@ -139,33 +138,33 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
// - username but no groupname, we use the user primary gid
var uid, gid int
if *username != "" {
u, osErr := user.Lookup(*username)
if osErr != nil {
SystemLog.Fatalf(userLookupErrStr, osErr)
u, err := user.Lookup(*username)
if err != nil {
SystemLog.Fatalf("Error looking up user: %s", err.Error())
}
uid, _ = strconv.Atoi(u.Uid)
gid, _ = strconv.Atoi(u.Gid)
}
if *groupname != "" {
g, osErr := user.LookupGroup(*groupname)
if osErr != nil {
SystemLog.Fatalf(groupLookupErrStr, osErr)
g, err := user.LookupGroup(*groupname)
if err != nil {
SystemLog.Fatalf("Error looking up group: %s", err.Error())
}
gid, _ = strconv.Atoi(g.Gid)
}
// If chroot provided, change to this!
if *chroot != "" {
osErr := syscall.Chroot(*chroot)
if osErr != nil {
SystemLog.Fatalf(chrootErrStr, osErr)
err := syscall.Chroot(*chroot)
if err != nil {
SystemLog.Fatalf("Error chrooting into directory: %s", err.Error())
}
SystemLog.Infof(chrootStr, *chroot)
SystemLog.Infof("Chrooting into dir: %s", *chroot)
// Ensure we're at root of chroot
osErr = os.Chdir("/")
if osErr != nil {
SystemLog.Fatalf(chDirErrStr, osErr)
err = os.Chdir("/")
if err != nil {
SystemLog.Fatalf("Error entering server directory: %s", err.Error())
}
}
@ -179,33 +178,33 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
}
// Change to server root
osErr := os.Chdir(Root)
if osErr != nil {
SystemLog.Fatalf(chDirErrStr, osErr)
err = os.Chdir(Root)
if err != nil {
SystemLog.Fatalf("Error entering server directory: %s", err.Error())
}
SystemLog.Infof(chDirStr, Root)
SystemLog.Infof("Entered server dir: %s", Root)
// If been supplied a group, change to requested group
if *groupname != "" {
osErr := syscall.Setgid(gid)
if osErr != nil {
SystemLog.Fatalf(setgidErrStr, osErr)
err := syscall.Setgid(gid)
if err != nil {
SystemLog.Fatalf("Error performing setgid: %s", err.Error())
}
SystemLog.Infof(setgidStr, *groupname)
SystemLog.Infof("Running as group: %s", *groupname)
}
// If been supplied a user, switch to requested user
if *username != "" {
osErr := syscall.Setuid(uid)
if osErr != nil {
SystemLog.Fatalf(setuidErrStr, osErr)
err := syscall.Setuid(uid)
if err != nil {
SystemLog.Fatalf("Error performing setuid: %s", err.Error())
}
SystemLog.Infof(setuidStr, *username)
SystemLog.Infof("Running as user: %s", *username)
}
// Check not running as root
if syscall.Geteuid() == 0 || syscall.Getegid() == 0 {
SystemLog.Fatalf(runningAsRootErrStr)
SystemLog.Fatal("Gophi does not support running as root!")
}
// FileSystemObject (and related) setup
@ -214,40 +213,41 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
// If no restricted paths provided, set to the disabled function. Else, compile and enable
if len(*restrictedPathsList) == 0 {
SystemLog.Info(pathRestrictionsDisabledStr)
SystemLog.Info("Path restrictions disabled")
IsRestrictedPath = isRestrictedPathDisabled
} else {
SystemLog.Info(pathRestrictionsEnabledStr)
SystemLog.Info("Path restrictions enabled")
restrictedPaths = compileRestrictedPathsRegex(*restrictedPathsList)
IsRestrictedPath = isRestrictedPathEnabled
}
// If no hidden paths provided, set to the disabled function. Else, compile and enable
if len(*hiddenPathsList) == 0 {
SystemLog.Info(pathHidingDisableStr)
SystemLog.Info("Path hiding disabled")
IsHiddenPath = isHiddenPathDisabled
} else {
SystemLog.Info(pathHidingEnabledStr)
SystemLog.Info("Path hiding enabled")
hiddenPaths = compileHiddenPathsRegex(*hiddenPathsList)
IsHiddenPath = isHiddenPathEnabled
}
// If no remapped paths provided, set to the disabled function. Else, compile and enable
if len(*remapRequestsList) == 0 {
SystemLog.Info(requestRemapDisabledStr)
SystemLog.Info("Request remapping disabled")
RemapRequest = remapRequestDisabled
} else {
SystemLog.Info(requestRemapEnabledStr)
SystemLog.Info("Request remapping enabled")
requestRemaps = compileRequestRemapRegex(*remapRequestsList)
RemapRequest = remapRequestEnabled
}
// If no CGI dir supplied, set to disabled function. Else, compile and enable
if *cgiDir == "" {
SystemLog.Info(cgiSupportDisabledStr)
SystemLog.Info("CGI script support disabled")
WithinCGIDir = withinCGIDirDisabled
} else {
SystemLog.Info(cgiSupportEnabledStr)
SystemLog.Info("CGI script support enabled")
SystemLog.Infof("CGI safe path: %s", *safePath)
cgiPath = NewSanitizedPathAtRoot(Root, *cgiDir)
cgiDirRegex = compileCGIRegex(cgiPath.Relative())
cgiEnv = setupInitialCGIEnv(*safePath)
@ -257,11 +257,11 @@ func ParseConfigAndSetup(tree config.Tree, proto string, defaultPort uint, newLi
// Set appropriate Path builder function depending
// on whether user spaces enabled or disabled
if *userSpacesEnabled {
SystemLog.Info(userSpacesEnabledStr)
SystemLog.Info("User spaces support enabled")
BuildPath = buildPathUserSpacesEnabled
SystemLog.Infof(userSpacesStr, "public_"+protocol)
SystemLog.Info("User space directory: public_" + protocol)
} else {
SystemLog.Info(userSpacesDisabledStr)
SystemLog.Info("User spaces support disabled")
BuildPath = buildPathUserSpacesDisabled
}
@ -287,7 +287,7 @@ func setupLogger(output string) *log.SLogger {
default:
file, err := os.OpenFile(output, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Fatalf(logOutputErrStr, output, err.Error())
log.Fatalf("Error opening log output %s: %s", output, err.Error())
}
return log.NewSLogger(file, true)
}

@ -61,20 +61,20 @@ func (c *conn) Conn() net.Conn {
}
// ReadLine reads a single line and returns the result, or nil and error
func (c *conn) ReadLine() ([]byte, errors.Error) {
func (c *conn) ReadLine() ([]byte, error) {
totalCount, end, emptyRead := 0, -1, 0
for {
// Perform a single read into the buffer
count, err := c.c.Read(c.b[totalCount:])
if err != nil {
return nil, ErrConnRead.Wrap(err)
return nil, errors.With(err).WrapWithin(ErrConnRead)
}
// Handle empty reads...
if count < 1 {
// After too many empty reads just return error
if !(emptyRead < 100) {
return nil, ErrConnRead.Wrap(io.ErrNoProgress)
return nil, errors.With(io.ErrNoProgress).WrapWithin(ErrConnRead)
}
// Iterate empty read counter
@ -107,23 +107,23 @@ func (c *conn) ReadLine() ([]byte, errors.Error) {
}
// WriteBytes writes a byte slice to the buffer and returns error status
func (c *conn) Write(b []byte) errors.Error {
func (c *conn) Write(b []byte) error {
_, err := c.bw.Write(b)
if err != nil {
return ErrConnWrite.Wrap(err)
return errors.With(err).WrapWithin(ErrConnWrite)
}
return nil
}
// ReadFrom writes to the buffer from a reader and returns error status
func (c *conn) ReadFrom(r io.Reader) errors.Error {
func (c *conn) ReadFrom(r io.Reader) error {
// Since this buffer wraps deadlineConn, which DOES NOT have
// a ReadFrom method implemented, it will force the buffer to
// use it's own internal byte buffer along with the deadlineConn's
// Write implementation (forcing the deadline to be regularly updated)
_, err := c.bw.ReadFrom(r)
if err != nil {
return ErrConnWrite.Wrap(err)
return errors.With(err).WrapWithin(ErrConnWrite)
}
return nil
}
@ -135,7 +135,7 @@ func (c *conn) Writer() io.Writer {
// Close flushes the underlying buffer, closes the conn then puts
// the sync.Pool conn buffers back
func (c *conn) Close() errors.Error {
func (c *conn) Close() error {
// Flush + close
c.bw.Flush()
err := c.c.Close()
@ -146,7 +146,7 @@ func (c *conn) Close() errors.Error {
// Return error (if exists)
if err != nil {
return ErrConnClose.Wrap(err)
return errors.With(err).WrapWithin(ErrConnClose)
}
return nil
}

@ -1,23 +1,29 @@
package core
import "github.com/grufwub/go-errors"
import (
"github.com/grufwub/go-errors"
)
// Core ErrorCodes
var (
ErrConnWrite = errors.New(connWriteErrStr)
ErrConnRead = errors.New(connReadErrStr)
ErrConnClose = errors.New(connCloseErrStr)
ErrListenerBegin = errors.New(listenerBeginErrStr)
ErrListenerAccept = errors.New(listenerAcceptErrStr)
ErrMutexUpgrade = errors.New(mutexUpgradeErrStr)
ErrMutexDowngrade = errors.New(mutexDowngradeErrStr)
ErrFileOpen = errors.New(fileOpenErrStr)
ErrFileStat = errors.New(fileStatErrStr)
ErrFileRead = errors.New(fileReadErrStr)
ErrFileType = errors.New(fileTypeErrStr)
ErrDirectoryRead = errors.New(directoryReadErrStr)
ErrRestrictedPath = errors.New(restrictedPathErrStr)
ErrInvalidRequest = errors.New(invalidRequestErrStr)
ErrCGIStart = errors.New(cgiStartErrStr)
ErrCGIExitCode = errors.New(cgiExitCodeErrStr)
ErrConnWrite = errors.BaseError("conn write error")
ErrConnRead = errors.BaseError("conn read error")
ErrConnClose = errors.BaseError("conn close error")
ErrListenerAccept = errors.BaseError("listener accept")
ErrMutexUpgrade = errors.BaseError("mutex upgrade fail")
ErrMutexDowngrade = errors.BaseError("mutex downgrade fail")
ErrFileOpen = errors.BaseError("file open error")
ErrFileStat = errors.BaseError("file stat error")
ErrFileRead = errors.BaseError("file read error")
ErrFileType = errors.BaseError("unsupported file type")
ErrDirectoryRead = errors.BaseError("directory read error")
ErrRestrictedPath = errors.BaseError("restricted path")
ErrUnescapingHost = errors.BaseError("unescaping host")
ErrUnescapingPath = errors.BaseError("unescaping path")
ErrParsingScheme = errors.BaseError("scheme parse fail")
ErrParsingHost = errors.BaseError("host parse fail")
ErrParsingURI = errors.BaseError("URI parse fail")
ErrInvalidRequest = errors.BaseError("invalid request")
ErrCGIStart = errors.BaseError("CGI start error")
ErrCGIExitCode = errors.BaseError("CGI non-zero exit code")
)

@ -2,14 +2,12 @@ package core
import (
"os"
"github.com/grufwub/go-errors"
)
// FileContent provides an interface for caching, rendering and getting cached contents of a file
type FileContent interface {
Load(*Path, *os.File) errors.Error
WriteToClient(*Client, *Path) errors.Error
Load(*Path, *os.File) error
WriteToClient(*Client, *Path) error
Clear()
}
@ -19,14 +17,14 @@ type RegularFileContent struct {
}
// Load takes an open FD and loads the file contents into FileContents memory
func (fc *RegularFileContent) Load(p *Path, file *os.File) errors.Error {
var err errors.Error
func (fc *RegularFileContent) Load(p *Path, file *os.File) error {
var err error
fc.content, err = ReadFile(file)
return err
}
// WriteToClient writes the current contents of FileContents to the client
func (fc *RegularFileContent) WriteToClient(client *Client, p *Path) errors.Error {
func (fc *RegularFileContent) WriteToClient(client *Client, p *Path) error {
return client.Conn().Write(fc.content)
}

@ -10,25 +10,25 @@ import (
)
// OpenFile opens a file for reading (read-only, world-readable)
func OpenFile(p *Path) (*os.File, errors.Error) {
func OpenFile(p *Path) (*os.File, error) {
file, err := os.OpenFile(p.Absolute(), os.O_RDONLY, 0444)
if err != nil {
return nil, ErrFileOpen.Wrap(err)
return nil, errors.With(err).WrapWithin(ErrFileOpen)
}
return file, nil
}
// StatFile performs a file stat on a file at path
func StatFile(p *Path) (os.FileInfo, errors.Error) {
func StatFile(p *Path) (os.FileInfo, error) {
stat, err := os.Stat(p.Absolute())
if err != nil {
return nil, ErrFileStat.Wrap(err)
return nil, errors.With(err).WrapWithin(ErrFileStat)
}
return stat, nil
}
// ReadFile reads a supplied file descriptor into a return byte slice, or error
func ReadFile(file *os.File) ([]byte, errors.Error) {
func ReadFile(file *os.File) ([]byte, error) {
// Get read buffers, defer putting back
br := fileBufferedReaderPool.Get(file)
defer fileBufferedReaderPool.Put(br)
@ -37,13 +37,13 @@ func ReadFile(file *os.File) ([]byte, errors.Error) {
buf := &bytes.Buffer{}
_, err := br.WriteTo(buf)
if err != nil {
return nil, ErrFileRead.Wrap(err)
return nil, errors.With(err).WrapWithin(ErrFileRead)
}
return buf.Bytes(), nil
}
// ScanFile scans a supplied file at file descriptor, using iterator function
func ScanFile(file *os.File, iterator func(string) bool) errors.Error {
func ScanFile(file *os.File, iterator func(string) bool) error {
// Get read buffer, defer putting back
br := fileBufferedReaderPool.Get(file)
defer fileBufferedReaderPool.Put(br)
@ -60,7 +60,7 @@ func ScanFile(file *os.File, iterator func(string) bool) errors.Error {
break
} else {
// Bad error, return
return ErrFileRead.Wrap(err)
return errors.With(err).WrapWithin(ErrFileRead)
}
}
@ -76,10 +76,10 @@ func ScanFile(file *os.File, iterator func(string) bool) errors.Error {
}
// ScanDirectory reads the contents of a directory and performs the iterator function on each os.FileInfo entry returned
func ScanDirectory(dir *os.File, p *Path, iterator func(os.FileInfo, *Path)) errors.Error {
func ScanDirectory(dir *os.File, p *Path, iterator func(os.FileInfo, *Path)) error {
nameList, err := dir.Readdirnames(-1)
if err != nil {
return ErrDirectoryRead.Wrap(err)
return errors.With(err).WrapWithin(ErrDirectoryRead)
}
// Sort by name

@ -17,10 +17,10 @@ func NewListener(l net.Listener) *Listener {
}
// Accept accepts a new connection and returns a client, or error
func (l *Listener) Accept() (*Client, errors.Error) {
func (l *Listener) Accept() (*Client, error) {
conn, err := l.l.Accept()
if err != nil {
return nil, ErrListenerAccept.Wrap(err)
return nil, errors.With(err).WrapWithin(ErrListenerAccept)
}
return NewClient(conn), nil
}

@ -1,7 +1,6 @@
package core
import (
"path"
"regexp"
"strings"
)
@ -17,10 +16,7 @@ type RequestRemap struct {
// compileCGIRegex takes a supplied string and returns compiled regular expression
func compileCGIRegex(cgiDir string) *regexp.Regexp {
if path.IsAbs(cgiDir) {
SystemLog.Fatalf(cgiDirNotRelativeStr)
}
SystemLog.Infof(cgiDirStr, cgiDir)
SystemLog.Infof("CGI directory: %s", cgiDir)
return regexp.MustCompile("^" + cgiDir + "(/.*)?$")
}
@ -38,12 +34,12 @@ func compileRestrictedPathsRegex(restrictions []string) []*regexp.Regexp {
// Compile the regular expression
regex, err := regexp.Compile("^" + expr + "$")
if err != nil {
SystemLog.Fatalf(pathRestrictRegexCompileFailStr, expr)
SystemLog.Fatalf("Failed compiling restricted path regex: %s", expr)
}
// Append compiled regex and log
regexes = append(regexes, regex)
SystemLog.Infof(pathRestrictRegexCompiledStr, expr)
SystemLog.Infof("Compiled restricted path regex: %s", expr)
}
return regexes
@ -63,12 +59,12 @@ func compileHiddenPathsRegex(hidden []string) []*regexp.Regexp {
// Compile the regular expression
regex, err := regexp.Compile("^" + expr + "$")
if err != nil {
SystemLog.Fatalf(pathHidingRegexCompileFailStr, expr)
SystemLog.Fatalf("Failed compiling hidden path regex: %s", expr)
}
// Append compiled regex and log
regexes = append(regexes, regex)
SystemLog.Infof(pathHidingRegexCompiledStr, expr)
SystemLog.Infof("Compiled hidden path regex: %s", expr)
}
return regexes
@ -88,18 +84,18 @@ func compileRequestRemapRegex(remaps []string) []*RequestRemap {
// Split into alias and remap
split := strings.Split(expr, requestRemapSeparatorStr)
if len(split) != 2 {
SystemLog.Fatalf(requestRemapRegexInvalidStr, expr)
SystemLog.Fatalf("Invalid request remap regex: %s", expr)
}
// Compile the regular expression
regex, err := regexp.Compile("^" + strings.TrimPrefix(split[0], "/") + "$")
if err != nil {
SystemLog.Fatalf(requestRemapRegexCompileFailStr, expr)
SystemLog.Fatalf("Failed compiling request remap regex: %s", expr)
}
// Append RequestRemap and log
requestRemaps = append(requestRemaps, &RequestRemap{regex, strings.TrimPrefix(split[1], "/")})
SystemLog.Infof(requestRemapRegexCompiledStr, expr)
SystemLog.Infof("Compiled path remap regex: %s", expr)
}
return requestRemaps

@ -14,7 +14,7 @@ import (
const (
// Version holds the current version string
Version = "v3.1.8"
Version = "v3.2.0-beta"
)
var (
@ -91,23 +91,23 @@ var (
// Global client-handling filesystem functions
newFileContent func(*Path) FileContent
handleDirectory func(*Client, *os.File, *Path) errors.Error
handleLargeFile func(*Client, *os.File, *Path) errors.Error
handleDirectory func(*Client, *os.File, *Path) error
handleLargeFile func(*Client, *os.File, *Path) error
)
// Start begins operation of the server
func Start(serve func(*Client)) {
// Start the FileCache freshness monitor
SystemLog.Infof(cacheMonitorStartStr, monitorSleepTime)
SystemLog.Infof("Starting cache monitor with freq: %s", monitorSleepTime.String())
go FileCache.StartMonitor(monitorSleepTime)
// Start the listener
SystemLog.Infof(listeningOnStr, protocol, Hostname, Port, Bind, Port)
SystemLog.Infof("Listening on %s://%s:%s (%s:%s)", protocol, Hostname, Port, Bind, Port)
go func() {
for {
client, err := serverListener.Accept()
if err != nil {
SystemLog.Errorf(err.Error())
SystemLog.Error(err.Error())
}
// Serve client then close in separate goroutine
@ -120,20 +120,20 @@ func Start(serve func(*Client)) {
// Listen for OS signals and terminate if necessary
sig := <-sigChannel
SystemLog.Infof(signalReceivedStr, sig)
SystemLog.Infof("Signal received: %s. Shutting down...", sig.String())
os.Exit(0)
}
// HandleClient handles a Client, attempting to serve their request from the filesystem whether a regular file, gophermap, dir listing or CGI script
func HandleClient(client *Client, request *Request) errors.Error {
func HandleClient(client *Client, request *Request) error {
// If restricted, return error
if IsRestrictedPath(request.Path()) {
return ErrRestrictedPath
return ErrRestrictedPath.Extend(request.Path().Selector())
}
// Try remap if necessary. If remapped, log!
if ok := RemapRequest(request); ok {
client.LogInfo(requestRemappedStr, request.Path().Selector(), request.Query())
client.LogInfo("Remapped request: %s %s", request.Path().Selector(), request.Query())
}
// If within CGI dir, attempt to execute this!
@ -163,9 +163,9 @@ func HandleClient(client *Client, request *Request) errors.Error {
defer file.Close()
// Get stat
stat, goErr := file.Stat()
stat, err := file.Stat()
if err != nil {
return ErrFileStat.Wrap(goErr)
return errors.With(err).WrapWithin(ErrFileStat)
}
switch {
@ -184,7 +184,7 @@ func HandleClient(client *Client, request *Request) errors.Error {
}
// FetchFile attempts to fetch a file from the cache, using the supplied file stat, Path and serving client. Returns Error status
func FetchFile(client *Client, file *os.File, stat os.FileInfo, p *Path) errors.Error {
func FetchFile(client *Client, file *os.File, stat os.FileInfo, p *Path) error {
// If file too big, write direct to client
if stat.Size() > fileSizeMax {
return handleLargeFile(client, file, p)

@ -1,85 +0,0 @@
package core
// Error string constants
const (
connWriteErrStr = "Conn write error"
connReadErrStr = "Conn read error"
connCloseErrStr = "Conn close error"
listenerBeginErrStr = "Listener begin error"
listenerAcceptErrStr = "Listener accept error"
mutexUpgradeErrStr = "Mutex upgrade fail"
mutexDowngradeErrStr = "Mutex downgrade fail"
fileOpenErrStr = "File open error"
fileStatErrStr = "File stat error"
fileReadErrStr = "File read error"
fileTypeErrStr = "Unsupported file type"
directoryReadErrStr = "Directory read error"
restrictedPathErrStr = "Restricted path"
invalidHostErrStr = "Invalid host"
invalidRequestErrStr = "Invalid request"
cgiStartErrStr = "CGI start error"
cgiExitCodeErrStr = "CGI non-zero exit code"
)
// Log string constants
const (
hostnameBindEmptyStr = "At least one of 'hostname' or 'listen' must be non-empty!"
chrootStr = "Chrooting into dir: %s"
chrootErrStr = "Error chrooting into directory: %s"
chDirStr = "Entered server dir: %s"
chDirErrStr = "Error entering server directory: %s"
rootDirEmptyErrStr = "No server root directory supplied!"
userLookupErrStr = "Error looking up user: %s"
groupLookupErrStr = "Error looking up group: %s"
currentUserLookupErrStr = "Error looking up current user: %s"
setuidStr = "Running as user: %s"
setuidErrStr = "Error performing setuid: %s"
setgidStr = "Running as group: %s"
setgidErrStr = "Error performing setgid: %s"
runningAsRootErrStr = "Gophi does not support running as root!"
listenerBeginFailStr = "Failed to start listener on %s://%s:%s (%s:%s) - %s"
listeningOnStr = "Listening on %s://%s:%s (%s:%s)"
cacheMonitorStartStr = "Starting cache monitor with freq: %s"
cacheFileStatErrStr = "Failed to stat file in cache: %s"
pathRestrictionsEnabledStr = "Path restrictions enabled"
pathRestrictionsDisabledStr = "Path restrictions disabled"
pathRestrictRegexCompileFailStr = "Failed compiling restricted path regex: %s"
pathRestrictRegexCompiledStr = "Compiled restricted path regex: %s"
pathHidingEnabledStr = "Path hiding enabled"
pathHidingDisableStr = "Path hiding disabled"
pathHidingRegexCompileFailStr = "Failed compiling hidden path regex: %s"
pathHidingRegexCompiledStr = "Compiled hidden path regex: %s"
requestRemapEnabledStr = "Request remapping enabled"
requestRemapDisabledStr = "Request remapping disabled"
requestRemapRegexInvalidStr = "Invalid request remap regex: %s"
requestRemapRegexCompileFailStr = "Failed compiling request remap regex: %s"
requestRemapRegexCompiledStr = "Compiled path remap regex: %s"
requestRemappedStr = "Remapped request: %s %s"
cgiPathStr = "CGI safe path: %s"
cgiSupportEnabledStr = "CGI script support enabled"
cgiSupportDisabledStr = "CGI script support disabled"
cgiDirNotRelativeStr = "CGI directory must be a relative path!"
cgiDirStr = "CGI directory: %s"
cgiExecuteErrStr = "Exit executing: %s [%d]"
userSpacesEnabledStr = "User spaces support enabled"
userSpacesDisabledStr = "User spaces support disabled"
userSpacesStr = "User space directory: %s"
signalReceivedStr = "Signal received: %v. Shutting down..."
logOutputErrStr = "Error opening log output %s: %s"
)

@ -17,7 +17,7 @@ func HasAsciiControlBytes(raw string) bool {
}
// ParseScheme attempts to parse a scheme from a raw url
func ParseScheme(raw string) (string, string, errors.Error) {
func ParseScheme(raw string) (string, string, error) {
// If first char is:
// - valid ascii (but non-scheme), return here no errors
// - end of scheme char, return bad request error
@ -26,7 +26,7 @@ func ParseScheme(raw string) (string, string, errors.Error) {
case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'):
// All good, continue
case c == ':':
return "", "", ErrInvalidRequest
return "", "", ErrParsingScheme.Extend(raw)
default:
// Invalid scheme char (or scheme first-char) return
return "", raw, nil
@ -104,12 +104,15 @@ func shouldHostEscape(b byte) bool {
func shouldPathEscape(b byte) bool {
switch b {
// Reserved character in path
case '?':
// Reserved character in path.
// Bear in mind ;, ARE allowed in a URL path,
// but when converting from a filesystem-->URL path
// (how this will be used), it will need escaping.
case '?', ';', ',':
return true
// Allowed in path
case '$', '&', '+', ',', '/', ':', ';', '=', '@':
case '$', '&', '+', '/', ':', '=', '@':
return false
// Check all-else
@ -136,7 +139,7 @@ func unescape(raw string, count int) string {
return t.String()
}
func unescapeHost(raw string) (string, errors.Error) {
func unescapeHost(raw string) (string, error) {
// Count all the percent signs
count := 0
for i := 0; i < len(raw); {
@ -147,14 +150,14 @@ func unescapeHost(raw string) (string, errors.Error) {
// If not a valid % encoded hex value, return with error
if i+2 >= len(raw) || !isHex(raw[i+1]) || !isHex(raw[i+2]) {
return "", ErrInvalidRequest.Extend("unescaping host info " + raw)
return "", ErrUnescapingHost.Extend(raw)
}
// In the host component % encoding can only be used
// for non-ASCII bytes. And rfc6874 introduces %25 for
// escaped percent sign in IPv6 literals
if unHex(raw[i+1]) < 8 && raw[i:i+3] != "%25" {
return "", ErrInvalidRequest.Extend("unescaping host " + raw)
return "", ErrUnescapingHost.Extend(raw)
}
// Skip iteration past the
@ -163,7 +166,7 @@ func unescapeHost(raw string) (string, errors.Error) {
default:
// If within ASCII range, and shoud be escaped, return error
if raw[i] < 0x80 && shouldHostEscape(raw[i]) {
return "", ErrInvalidRequest.Extend("unescaping host " + raw)
return "", ErrUnescapingHost.Extend(raw)
}
// Iter
@ -178,7 +181,7 @@ func unescapeHost(raw string) (string, errors.Error) {
return unescape(raw, count), nil
}
func unescapePath(raw string) (string, errors.Error) {
func unescapePath(raw string) (string, error) {
// Count all the percent signs
count := 0
length := len(raw)
@ -190,7 +193,7 @@ func unescapePath(raw string) (string, errors.Error) {
// If not a valid % encoded hex value, return with error
if i+2 >= length || !isHex(raw[i+1]) || !isHex(raw[i+2]) {
return "", ErrInvalidRequest.Extend("unescaping path " + raw)
return "", ErrUnescapingPath.Extend(raw)
}
// Skip iteration past the
@ -240,11 +243,11 @@ func EscapePath(path string) string {
}
// ParseEncodedHost parses encoded host info, safely returning unescape host and port
func ParseEncodedHost(raw string) (string, string, errors.Error) {
func ParseEncodedHost(raw string) (string, string, error) {
// Unescape the host info
raw, err := unescapeHost(raw)
if err != nil {
return "", "", err
return "", "", err.(errors.Error).WrapWithin(ErrParsingHost)
}
// Split by last ':' and return
@ -253,14 +256,14 @@ func ParseEncodedHost(raw string) (string, string, errors.Error) {
}
// ParseEncodedURI parses encoded URI, safely returning unescaped path and still-escaped query
func ParseEncodedURI(received string) (string, string, errors.Error) {
func ParseEncodedURI(received string) (string, string, error) {
// Split into path and query
rawPath, query := SplitBy(received, "?")
// Unescape path, query is up-to CGI scripts
rawPath, err := unescapePath(rawPath)
if err != nil {
return "", "", ErrInvalidRequest.Wrap(err)
return "", "", err.(errors.Error).WrapWithin(ErrParsingURI)
}
// Return the raw path and query

@ -0,0 +1,67 @@
package core_test
import (
"fmt"
"gophi/core"
"io/ioutil"
"net/url"
"testing"
)
var toEscape = []string{
"",
"abc",
"abc+def",
"a?b",
"one two",
"10%",
" ?&=#+%!<>#\"{}|\\^[]`☺\t:@$'()*,;",
}
var escaped = []string{
"",
"abc",
"abc+def",
"a%3Fb",
"one%20two",
"10%25",
"%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:@$%27%28%29%2A%2C%3B",
}
func TestPathEscape(t *testing.T) {
for i, path := range toEscape {
if escapedPath := core.EscapePath(path); escapedPath != escaped[i] {
t.Fatalf("Failed escaping path!\nGot: %s\nExpected: %s\n", escapedPath, escaped[i])
}
}
}
func TestParseEncodedHost(t *testing.T) {
}
func TestParseEncodedURI(t *testing.T) {
}
func BenchmarkCorePathEscape(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
for _, path := range toEscape {
s = core.EscapePath(path)
}
}
fmt.Fprint(ioutil.Discard, s)
}
func BenchmarkURLPathEscape(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
for _, path := range toEscape {
// This isn't *exactly* a fair comparison to core.EscapePath,
// since url.PathEscape() escapes a path _segment_ as opposed
// to any entire path, whereas core.EscapePath() escapes an entire
// filesystem path.
s = url.PathEscape(path)
}
}
fmt.Fprint(ioutil.Discard, s)
}

@ -8,51 +8,88 @@ import (
// Gemini specific base errors
var (
errInvalidTLSConfig = errors.New(invalidTLSConfigErrStr)
errInvalidProtocol = errors.New(inavlidProtocolErrStr)
errInvalidHostPort = errors.New(invalidHostPortErrStr)
errInvalidScheme = errors.BaseError("invalid request scheme")
errProxyRequest = errors.BaseError("host:port pair differ from our own")
)
// Gemini status codes
var (
statusInput = "10"
statusSensitive = "11"
statusTemporaryRedirect = "30"
statusPermanentRedirect = "31"
statusTemporaryFailure = "40"
statusServerUnavailable = "41"
statusCGIError = "42"
statusProxyError = "43"
statusSlowDown = "44"
statusPermanentFailure = "50"
statusNotFound = "51"
statusGone = "52"
statusProxyRequestRefused = "53"
statusBadRequest = "59"
statusClientCertificateRequired = "60"
statusClientCertificateNotAuthorized = "61"
statusCertificateNotValid = "62"
)
// Gemini error responses
var (
// more specific respnoses
errConnReadRsp = buildResponseHeader(statusTemporaryFailure, "Read Failure")
errRestrictedRsp = buildResponseHeader(statusNotFound, "Restricted Path")
errInvalidSchemeRsp = buildResponseHeader(statusProxyRequestRefused, "Unsupported Scheme")
errProxyRequestRsp = buildResponseHeader(statusProxyRequestRefused, "Proxying Unsupported")
// generic responses
errNotFoundRsp = buildResponseHeader(statusNotFound, "Not Found")
errTemporaryFailureRsp = buildResponseHeader(statusTemporaryFailure, "Temporary Failure")
errPermanentFailureRsp = buildResponseHeader(statusPermanentFailure, "Permanent Failure")
errInvalidRequestRsp = buildResponseHeader(statusBadRequest, "Invalid Request")
)
// generateErrorResponse takes an error code and generates an error response byte slice
func generateErrorResponse(err errors.Error) ([]byte, bool) {
func generateErrorResponse(err error) ([]byte, bool) {
switch {
case err.Equals(core.ErrConnWrite):
case errors.Is(err, core.ErrConnWrite):
return nil, false // no point responding if we couldn't write
case err.Equals(core.ErrConnRead):
return buildErrorResponse("40", statusMeta40), true
case err.Equals(core.ErrConnClose):
case errors.Is(err, core.ErrConnRead):
return errConnReadRsp, true
case errors.Is(err, core.ErrConnClose):
return nil, false // no point responding if we couldn't close
case err.Equals(core.ErrMutexUpgrade):
return buildErrorResponse("40", statusMeta40), true
case err.Equals(core.ErrMutexDowngrade):
return buildErrorResponse("40", statusMeta40), true
case err.Equals(core.ErrFileOpen):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrFileStat):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrFileRead):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrFileType):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrDirectoryRead):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrRestrictedPath):
return buildErrorResponse("51", statusMeta51), true
case err.Equals(core.ErrInvalidRequest):
return buildErrorResponse("59", statusMeta59), true
case err.Equals(core.ErrCGIStart):
return buildErrorResponse("42", statusMeta42), true
case err.Equals(core.ErrCGIExitCode):
return buildErrorResponse("42", statusMeta42), true
case err.Equals(errInvalidProtocol):
return buildErrorResponse("53", statusMeta59), true
case err.Equals(errInvalidHostPort):
return buildErrorResponse("53", statusMeta53), true
case errors.Is(err, core.ErrMutexUpgrade):
return errTemporaryFailureRsp, true
case errors.Is(err, core.ErrMutexDowngrade):
return errTemporaryFailureRsp, true
case errors.Is(err, core.ErrFileOpen):
return errNotFoundRsp, true
case errors.Is(err, core.ErrFileStat):
return errNotFoundRsp, true
case errors.Is(err, core.ErrFileRead):
return errNotFoundRsp, true
case errors.Is(err, core.ErrFileType):
return errNotFoundRsp, true
case errors.Is(err, core.ErrDirectoryRead):
return errNotFoundRsp, true
case errors.Is(err, core.ErrRestrictedPath):
return errRestrictedRsp, true
case errors.Is(err, core.ErrInvalidRequest):
return errInvalidRequestRsp, true
case errors.Is(err, core.ErrParsingScheme):
return errInvalidRequestRsp, true
case errors.Is(err, core.ErrParsingHost):
return errInvalidRequestRsp, true
case errors.Is(err, core.ErrParsingURI):
return errInvalidRequestRsp, true
case errors.Is(err, core.ErrCGIStart):
return errPermanentFailureRsp, true
case errors.Is(err, core.ErrCGIExitCode):
return errTemporaryFailureRsp, true
case errors.Is(err, errInvalidScheme):
return errInvalidSchemeRsp, true
case errors.Is(err, errProxyRequest):
return errProxyRequestRsp, true
default:
return nil, false
}
}
func buildErrorResponse(statusCode, statusMeta string) []byte {
return buildResponseHeader(statusCode, statusMeta)
}

@ -3,8 +3,6 @@ package gemini
import (
"gophi/core"
"os"
"github.com/grufwub/go-errors"
)
type headerPlusFileContent struct {
@ -12,12 +10,12 @@ type headerPlusFileContent struct {
}
// WriteToClient writes the current contents of FileContents to the client
func (fc *headerPlusFileContent) WriteToClient(client *core.Client, p *core.Path) errors.Error {
func (fc *headerPlusFileContent) WriteToClient(client *core.Client, p *core.Path) error {
return client.Conn().Write(fc.contents)
}
// Load takes an open FD and loads the file contents into FileContents memory
func (fc *headerPlusFileContent) Load(p *core.Path, file *os.File) errors.Error {
func (fc *headerPlusFileContent) Load(p *core.Path, file *os.File) error {
// Read the file contents
contents, err := core.ReadFile(file)
if err != nil {

@ -7,7 +7,6 @@ import (
"io"
"github.com/grufwub/go-config"
"github.com/grufwub/go-errors"
"github.com/grufwub/go-logger"
)
@ -16,7 +15,7 @@ func init() {
b := make([]byte, 1)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
logger.Fatal(entropyAssertFailStr)
logger.Fatal("Failed to assert safe source of system entropy exists!")
}
}
@ -32,11 +31,11 @@ func Run() {
tree,
"gemini",
1965,
func() (*core.Listener, errors.Error) {
func() (*core.Listener, error) {
// Load the supplied key pair
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
return nil, errInvalidTLSConfig.Wrap(err)
return nil, err
}
// Create TLS config
@ -48,7 +47,7 @@ func Run() {
// Create listener!
l, err := tls.Listen("tcp", core.Bind+":"+core.Port, config)
if err != nil {
return nil, core.ErrListenerBegin.Wrap(err)
return nil, err
}
// Return wrapper listener
@ -61,6 +60,7 @@ func Run() {
)
// Generate the root redirect byte slice
// (has to be done here once the Hostname and Port have been set)
rootRedirect = buildRedirect("gemini://" + core.Hostname + ":" + core.Port + "/")
// Start!

@ -4,8 +4,6 @@ import (
"gophi/core"
"os"
"strings"
"github.com/grufwub/go-errors"
)
// rootRedirectHeader stores the root redirect header byte slice,
@ -17,7 +15,7 @@ func serve(client *core.Client) {
// Receive line from client
received, err := client.Conn().ReadLine()
if err != nil {
client.LogError(clientReadFailStr)
client.LogError("Conn read fail")
handleError(client, err)
return
}
@ -25,27 +23,25 @@ func serve(client *core.Client) {
// Ensure is a valid URL string
if core.HasAsciiControlBytes(raw) {
client.LogError(invalidRequestStr, raw)
handleError(client, core.ErrInvalidRequest)
client.LogError("Invalid request: %s", raw)
handleError(client, core.ErrInvalidRequest.Extend("has ascii control bytes"))
return
}
// Get the URL scheme (or error!)
scheme, path, err := core.ParseScheme(raw)
if err != nil {
client.LogError(invalidRequestStr, raw)
client.LogError("Invalid request: %s", raw)
handleError(client, err)
return
}
// Infer no schema as 'gemini', else check we
// were explicitly provided 'gemini'
if scheme != "" {
if scheme != "gemini" {
client.LogError(invalidRequestStr, raw)
handleError(client, errInvalidProtocol.Extend(scheme))
return
}
if scheme != "" && scheme != "gemini" {
client.LogError("Invalid request: %s", raw)
handleError(client, errInvalidScheme.Extend(scheme))
return
}
// Split by first '/' (with prefix '//' trimmed) to get host info and path strings
@ -54,29 +50,29 @@ func serve(client *core.Client) {
// Parse the URL encoded host info
host, port, err := core.ParseEncodedHost(host)
if err != nil {
client.LogError(invalidRequestStr, raw)
client.LogError("Invalid request: %s", raw)
handleError(client, err)
return
}
// Check the host and port are our own (empty port is allowed)
if host != core.Hostname || (port != "" && port != core.Port) {
client.LogError(invalidRequestStr, raw)
handleError(client, errInvalidHostPort.Extend(host+":"+port))
client.LogError("Invalid request: %s", raw)
handleError(client, errProxyRequest.Extend(host+":"+port))
return
}
// Parse the encoded URI into path and query components
path, query, err := core.ParseEncodedURI(path)
if err != nil {
client.LogError(invalidRequestStr, raw)
client.LogError("Invalid request: %s", raw)
handleError(client, err)
return
}
// Redirect empty path to root
if len(path) < 1 {
client.LogInfo(clientRedirectStr, "/")
client.LogInfo("Redirect to: /")
client.Conn().Write(rootRedirect)
return
}
@ -88,37 +84,37 @@ func serve(client *core.Client) {
err = core.HandleClient(client, request)
if err != nil {
handleError(client, err)
client.LogError(clientServeFailStr, request.String())
client.LogError("Failed to serve: %s", request.String())
} else {
client.LogInfo(clientServedStr, request.String())
client.LogInfo("Served: %s", request.String())
}
}
// handleError determines whether to send an error response to the client, and logs to system
func handleError(client *core.Client, err errors.Error) {
func handleError(client *core.Client, err error) {
response, ok := generateErrorResponse(err)
if ok {
client.Conn().Write(response)
}
core.SystemLog.Errorf(err.Error())
core.SystemLog.Error(err.Error())
}
func handleDirectory(client *core.Client, file *os.File, p *core.Path) errors.Error {
func handleDirectory(client *core.Client, file *os.File, p *core.Path) error {
// First check for index gem, create gem Path object
indexGem := p.JoinPathUnsafe("index.gmi")
// If index gem exists, we fetch this
fd2, err := core.OpenFile(indexGem)
file2, err := core.OpenFile(indexGem)
if err == nil {
stat, osErr := fd2.Stat()
if osErr == nil {
stat, err := file2.Stat()
if err == nil {
// Fetch gem and defer close
defer fd2.Close()
return core.FetchFile(client, fd2, stat, indexGem)
defer file2.Close()
return core.FetchFile(client, file2, stat, indexGem)
}
// Else, just close fd2
fd2.Close()
file2.Close()
}
// Slice to write
@ -159,7 +155,7 @@ func handleDirectory(client *core.Client, file *os.File, p *core.Path) errors.Er
return client.Conn().Write(append(header, []byte(dirContents)...))
}
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) errors.Error {
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) error {
// Build the response header
header := buildResponseHeader("20", getFileStatusMeta(p))

@ -1,39 +0,0 @@
package gemini
// Client error response strings
const (
statusMeta10 = "Input"
statusMeta11 = "Sensitive Input"
statusMeta30 = "Temporary Redirect"
statusMeta31 = "Permanent Redirect"
statusMeta40 = "Temporary Failure"
statusMeta41 = "Server Unavailable"
statusMeta42 = "CGI Error"
statusMeta43 = "Proxy Error"
statusMeta44 = "Slow Down"
statusMeta50 = "Permanent Failure"
statusMeta51 = "Not Found"
statusMeta52 = "Gone"
statusMeta53 = "Proxy Request Refused"
statusMeta59 = "Bad Request"
statusMeta60 = "Client Certificate Required"
statusMeta61 = "Client Certificate Not Authorised"
statusMeta62 = "Certificate Not Valid"
)
// Gemini specific error string constants
const (
invalidTLSConfigErrStr = "Invalid TLS cert or key file"
inavlidProtocolErrStr = "Invalid request protocol"
invalidHostPortErrStr = "Invalid host:port pair"
)
// Log string constants
const (
entropyAssertFailStr = "Failed to assert safe source of system entropy exists!"
clientReadFailStr = "Failed to read"
invalidRequestStr = "Invalid request: %s"
clientServeFailStr = "Failed to serve: %s"
clientServedStr = "Served: %s"
clientRedirectStr = "Redirect to: %s"
)

@ -5,7 +5,7 @@ go 1.15
require (
github.com/grufwub/go-bufpools v0.1.1
github.com/grufwub/go-config v0.1.0
github.com/grufwub/go-errors v0.1.0
github.com/grufwub/go-errors v0.3.1
github.com/grufwub/go-filecache v0.1.0
github.com/grufwub/go-logger v0.1.1
)

@ -4,8 +4,8 @@ github.com/grufwub/go-bufpools v0.1.1 h1:TOUKNY+UaQ784EtvP+wKoXssYQQRX5zewtrZ7u4
github.com/grufwub/go-bufpools v0.1.1/go.mod h1:ITqLRtG+W1bZHGdkWewV7inb+GcWfq2Jcjqx4AZ7aBY=
github.com/grufwub/go-config v0.1.0 h1:/UDEmprs4h4qEkgmQqthmtGZeJs8eB44qMVSqa+5sxU=
github.com/grufwub/go-config v0.1.0/go.mod h1:0U5Y0EkNeL09YkY70fNZv4Kelfayp/VroEs2UzmUG04=
github.com/grufwub/go-errors v0.1.0 h1:1gNPO4zzVDS88Kgtd60cBs7ZJiWy1hjjP6bFG6Ik+t8=
github.com/grufwub/go-errors v0.1.0/go.mod h1:AXGtU2fWv8ejaUUT0+9wTOlWqcxYDo8wuYnhrYtoBKM=
github.com/grufwub/go-errors v0.3.1 h1:Q0N5njkkFgAquX26MmNctonYD86htRly8yEZyWIhF+E=
github.com/grufwub/go-errors v0.3.1/go.mod h1:AXGtU2fWv8ejaUUT0+9wTOlWqcxYDo8wuYnhrYtoBKM=
github.com/grufwub/go-filecache v0.1.0 h1:OugzIHzLco8LLRnAlD7m6zSFQTILjltNO8Hhr/8vcCo=
github.com/grufwub/go-filecache v0.1.0/go.mod h1:iAfqEfsC5YsyGD+f8JducuWeRqCDBVPi1+VmCaPL07Q=
github.com/grufwub/go-logger v0.1.1 h1:KnD6NNyeq3cz6dZKW/Gr+Fz9dNvkLf8KvYZXKGM5cN0=

@ -8,47 +8,52 @@ import (
// Gopher specific error codes
var (
errInvalidGophermap = errors.New(invalidGophermapErrStr)
errSubgophermapIsDir = errors.New(subgophermapIsDirErrStr)
errSubgophermapSize = errors.New(subgophermapSizeErrStr)
errInvalidGophermap = errors.Error(invalidGophermapErrStr)
errSubgophermapIsDir = errors.Error(subgophermapIsDirErrStr)
errSubgophermapSize = errors.Error(subgophermapSizeErrStr)
)
// generateErrorResponse takes an error code and generates an error response byte slice
func generateErrorResponse(err errors.Error) ([]byte, bool) {
func generateErrorResponse(err error) ([]byte, bool) {
switch {
case err.Equals(core.ErrConnWrite):
case errors.Is(err, core.ErrConnWrite):
return nil, false // no point responding if we couldn't write
case err.Equals(core.ErrConnRead):
case errors.Is(err, core.ErrConnRead):
return buildErrorLine(errorResponse503), true
case err.Equals(core.ErrConnClose):
case errors.Is(err, core.ErrConnClose):
return nil, false // no point responding if we couldn't close
case err.Equals(core.ErrMutexUpgrade):
case errors.Is(err, core.ErrMutexUpgrade):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrMutexDowngrade):
case errors.Is(err, core.ErrMutexDowngrade):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrFileOpen):
case errors.Is(err, core.ErrFileOpen):
return buildErrorLine(errorResponse404), true
case err.Equals(core.ErrFileStat):
case errors.Is(err, core.ErrFileStat):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrFileRead):
case errors.Is(err, core.ErrFileRead):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrFileType):
case errors.Is(err, core.ErrFileType):
return buildErrorLine(errorResponse404), true
case err.Equals(core.ErrDirectoryRead):
case errors.Is(err, core.ErrDirectoryRead):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrRestrictedPath):
case errors.Is(err, core.ErrRestrictedPath):
return buildErrorLine(errorResponse403), true
case err.Equals(core.ErrInvalidRequest):
// All forms of invalid request
case errors.Is(err, core.ErrInvalidRequest):
return buildErrorLine(errorResponse400), true
case errors.Is(err, core.ErrParsingScheme):
return buildErrorLine(errorResponse400), true
case err.Equals(core.ErrCGIStart):
case errors.Is(err, core.ErrCGIStart):
return buildErrorLine(errorResponse500), true
case err.Equals(core.ErrCGIExitCode):
case errors.Is(err, core.ErrCGIExitCode):
return buildErrorLine(errorResponse500), true
case err.Equals(errInvalidGophermap):
case errors.Is(err, errInvalidGophermap):
return buildErrorLine(errorResponse500), true
case err.Equals(errSubgophermapIsDir):
case errors.Is(err, errSubgophermapIsDir):
return buildErrorLine(errorResponse500), true
case err.Equals(errSubgophermapSize):
case errors.Is(err, errSubgophermapSize):
return buildErrorLine(errorResponse500), true
default:
return nil, false

@ -3,8 +3,6 @@ package gopher
import (
"gophi/core"
"os"
"github.com/grufwub/go-errors"
)
// generatedFileContents is a simple core.FileContent implementation for holding onto a generated (virtual) file contents
@ -13,10 +11,10 @@ type generatedFileContent struct {
}
// ReadAllFrom does nothing for generated content
func (fc *generatedFileContent) Load(p *core.Path, file *os.File) errors.Error { return nil }
func (fc *generatedFileContent) Load(p *core.Path, file *os.File) error { return nil }
// WriteAllTo writes the generated FileContent to client
func (fc *generatedFileContent) WriteToClient(client *core.Client, p *core.Path) errors.Error {
func (fc *generatedFileContent) WriteToClient(client *core.Client, p *core.Path) error {
return client.Conn().Write(fc.content)
}
@ -29,14 +27,14 @@ type gophermapContent struct {
}
// Load takes an open FD and loads the gophermap contents into memory as different renderable sections
func (gc *gophermapContent) Load(path *core.Path, file *os.File) errors.Error {
var err errors.Error
func (gc *gophermapContent) Load(path *core.Path, file *os.File) error {
var err error
gc.sections, err = readGophermap(file, path)
return err
}
// WriteToClient renders each cached section of the gophermap, and writes them to the client
func (gc *gophermapContent) WriteToClient(client *core.Client, path *core.Path) errors.Error {
func (gc *gophermapContent) WriteToClient(client *core.Client, path *core.Path) error {
// Render + write the sections!
for _, section := range gc.sections {
err := section.RenderAndWrite(client)

@ -17,9 +17,9 @@ const (
// formatName formats a gopher line name string
func formatName(name string) string {
if len(name) > pageWidth {
return name[:pageWidth-4] + "...\t"
return name[:pageWidth-4] + "..."
}
return name + "\t"
return name
}
// formatSelector formats a gopher line selector string
@ -27,7 +27,7 @@ func formatSelector(selector string) string {
if len(selector) > maxSelectorLen {
return errorSelector
}
return selector
return core.EscapePath(selector)
}
// replacePlacementStrs replaces any placement strings found in the line (e.g $hostname, $port)
@ -48,12 +48,12 @@ func replacePlacementStrs(line string) string {
// buildLine builds a gopher line string
func buildLine(t ItemType, name, selector, host, port string) []byte {
return []byte(string(t) + formatName(name) + formatSelector(selector) + "\t" + host + "\t" + port + "\r\n")
return []byte(string(t) + formatName(name) + "\t" + formatSelector(selector) + "\t" + host + "\t" + port + "\r\n")
}
// buildInfoLine builds a gopher info line string
func buildInfoLine(line string) []byte {
return []byte(string(typeInfo) + formatName(line) + "\t" + nullHost + "\t" + nullPort + "\r\n")
return []byte(string(typeInfo) + formatName(line) + "\t\t" + nullHost + "\t" + nullPort + "\r\n")
}
// buildErrorLine builds a gopher error line string

@ -3,17 +3,18 @@ package gopher
import (
"gophi/core"
"os"
"strconv"
"github.com/grufwub/go-errors"
)
// GophermapSection is an interface that specifies individually renderable (and writeable) sections of a gophermap
type gophermapSection interface {
RenderAndWrite(*core.Client) errors.Error
RenderAndWrite(*core.Client) error
}
// readGophermap reads a FD and Path as gophermap sections
func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, errors.Error) {
func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, error) {
// Create return slice
sections := make([]gophermapSection, 0)
@ -23,7 +24,7 @@ func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, errors.Erro
}
// Declare variables
var returnErr errors.Error
var returnErr error
titleAlready := false
// Perform scan of gophermap FD
@ -82,10 +83,10 @@ func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, errors.Erro
// Get stat
stat, err := subFile.Stat()
if err != nil {
returnErr = core.ErrFileStat.Wrap(err)
returnErr = errors.WrapError(core.ErrFileStat, err)
return false
} else if stat.IsDir() {
returnErr = errSubgophermapIsDir
returnErr = errors.Wrap(request.Path().Absolute(), errSubgophermapIsDir)
return false
}
@ -97,7 +98,7 @@ func readGophermap(file *os.File, p *core.Path) ([]gophermapSection, errors.Erro
// Error out if file too big
if stat.Size() > subgophermapSizeMax {
returnErr = errSubgophermapSize
returnErr = errors.Wrap(strconv.FormatInt(stat.Size(), 10), errSubgophermapSize)
return false
}
@ -145,7 +146,7 @@ type TextSection struct {
}
// RenderAndWrite simply writes the byte slice to the client
func (s *TextSection) RenderAndWrite(client *core.Client) errors.Error {
func (s *TextSection) RenderAndWrite(client *core.Client) error {
return client.Conn().Write(s.contents)
}
@ -156,7 +157,7 @@ type DirectorySection struct {
}
// RenderAndWrite scans and renders a list of the contents of a directory (skipping hidden or restricted files)
func (s *DirectorySection) RenderAndWrite(client *core.Client) errors.Error {
func (s *DirectorySection) RenderAndWrite(client *core.Client) error {
file, err := core.OpenFile(s.path)
if err != nil {
return err
@ -190,7 +191,7 @@ type FileSection struct {
}
// RenderAndWrite simply opens, reads and writes the file contents to the client
func (s *FileSection) RenderAndWrite(client *core.Client) errors.Error {
func (s *FileSection) RenderAndWrite(client *core.Client) error {
// Open FD for the file
file, err := core.OpenFile(s.path)
if err != nil {
@ -222,7 +223,7 @@ type SubgophermapSection struct {
}
// RenderAndWrite reads, renders and writes the contents of the gophermap to the client
func (s *SubgophermapSection) RenderAndWrite(client *core.Client) errors.Error {
func (s *SubgophermapSection) RenderAndWrite(client *core.Client) error {
// Get FD for gophermap
file, err := core.OpenFile(s.path)
if err != nil {
@ -252,6 +253,6 @@ type CGISection struct {
}
// RenderAndWrite takes the request, and executes the associated CGI script with parameters
func (s *CGISection) RenderAndWrite(client *core.Client) errors.Error {
func (s *CGISection) RenderAndWrite(client *core.Client) error {
return core.TryExecuteCGIScript(client, s.request)
}

@ -5,7 +5,6 @@ import (
"net"
"github.com/grufwub/go-config"
"github.com/grufwub/go-errors"
"github.com/grufwub/go-filecache"
)
@ -25,10 +24,10 @@ func Run() {
tree,
"gopher",
70,
func() (*core.Listener, errors.Error) {
func() (*core.Listener, error) {
l, err := net.Listen("tcp", core.Bind+":"+core.Port)
if err != nil {
return nil, core.ErrListenerBegin.Wrap(err)
return nil, err
}
return core.NewListener(l), nil
},

@ -4,8 +4,6 @@ import (
"gophi/core"
"os"
"strings"
"github.com/grufwub/go-errors"
)
var (
@ -69,7 +67,7 @@ func serve(client *core.Client) {
}
}
func handleDirectory(client *core.Client, file *os.File, p *core.Path) errors.Error {
func handleDirectory(client *core.Client, file *os.File, p *core.Path) error {
// First check for gophermap, create gophermap Path object
gophermap := p.JoinPathUnsafe("gophermap")
@ -90,10 +88,13 @@ func handleDirectory(client *core.Client, file *os.File, p *core.Path) errors.Er
// Slice to write
dirContents := make([]byte, 0)
// Escape the previous dir
dirSel := core.EscapePath(p.SelectorDir())
// Add directory heading, empty line and a back line
dirContents = append(dirContents, buildLine(typeInfo, "[ "+core.Hostname+p.Selector()+" ]", "TITLE", nullHost, nullPort)...)
dirContents = append(dirContents, buildInfoLine("")...)
dirContents = append(dirContents, buildLine(typeDirectory, "..", p.SelectorDir(), core.Hostname, core.Port)...)
dirContents = append(dirContents, buildLine(typeDirectory, "..", dirSel, core.Hostname, core.Port)...)
// Scan directory and build lines
err = core.ScanDirectory(
@ -113,12 +114,12 @@ func handleDirectory(client *core.Client, file *os.File, p *core.Path) errors.Er
return client.Conn().Write(dirContents)
}
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) errors.Error {
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) error {
return client.Conn().ReadFrom(file)
}
// handleError determines whether to send an error response to the client, and logs to system
func handleError(client *core.Client, err errors.Error) {
func handleError(client *core.Client, err error) {
response, ok := generateErrorResponse(err)
if ok {
client.Conn().Write(response)

Loading…
Cancel
Save