From 352bcb67f6c08e7d134a866d4336fadb15487ba9 Mon Sep 17 00:00:00 2001 From: "kim (grufwub)" Date: Sun, 12 Apr 2020 21:32:09 +0100 Subject: [PATCH] big code refactoring + work on supporting new item types Signed-off-by: kim (grufwub) --- entities.go | 59 ++++++++++++++++- gophor.go | 16 +---- worker.go | 186 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 205 insertions(+), 56 deletions(-) diff --git a/entities.go b/entities.go index 2f7015f..d308e00 100644 --- a/entities.go +++ b/entities.go @@ -46,6 +46,8 @@ const ( TypeRedundant = ItemType('+') /* Redundant server */ + TypeEnd = ItemType('.') /* Indicates LastLine if only this + CrLf */ + /* Non-standard - as used by https://github.com/prologic/go-gopher * (also seen on Wikipedia: https://en.wikipedia.org/wiki/Gopher_%28protocol%29#Item_types) */ @@ -54,13 +56,24 @@ const ( TypeAudio = ItemType('s') /* Audio file */ TypePng = ItemType('p') /* PNG image */ TypeDoc = ItemType('d') /* Document [DOC] */ - + /* Non-standard - as used by Gopernicus https://github.com/gophernicus/gophernicus */ TypeMime = ItemType('M') /* [MIME] */ + TypeVideo = ItemType(';') /* [VIDEO] */ + TypeCalendar = ItemType('c') /* [CALENDAR] */ TypeTitle = ItemType('!') /* [TITLE] */ + TypeComment = ItemType('#') /* [COMMENT] */ + TypeHiddenFile = ItemType('-') /* [HIDDEN] Hides file from directory listing */ + TypeSubGophermap = ItemType('=') /* [EXECUTE] read this file in here */ + TypeEndBeginList = ItemType('*') /* If only this + CrLf, indicates last line but then followed by directory list */ /* Default type */ TypeDefault = TypeFile + + /* Gophor specific types */ + TypeExec = ItemType('$') /* Execute command and insert stdout here */ + TypeInfoNotStated = ItemType('z') /* INTERNAL USE. We use this in a switch case, a line never starts with this */ + TypeUnknown = ItemType('?') /* INTERNAL USE. We use this in a switch case, a line never starts with this */ ) /* @@ -151,3 +164,47 @@ func getItemType(name string) ItemType { } return fileType } + +func parseLineType(line string) ItemType { + lineLen := len(line) + + if lineLen == 0 { + return TypeInfoNotStated + } else if lineLen == 1 { + /* The only accepted types for a length 1 line */ + switch ItemType(line[0]) { + case TypeEnd: + return TypeEnd + case TypeEndBeginList: + return TypeEndBeginList + case TypeComment: + return TypeComment + case TypeInfo: + return TypeInfo + case TypeTitle: + return TypeTitle + default: + return TypeUnknown + } + } else if !strings.Contains(line, string(Tab)) { + /* The only accepted types for a line with no tabs */ + switch ItemType(line[0]) { + case TypeComment: + return TypeComment + case TypeTitle: + return TypeTitle + case TypeInfo: + return TypeInfo + case TypeHiddenFile: + return TypeHiddenFile + case TypeSubGophermap: + return TypeSubGophermap + case TypeExec: + return TypeExec + default: + return TypeInfoNotStated + } + } + + return ItemType(line[0]) +} diff --git a/gophor.go b/gophor.go index f2b1758..236828b 100644 --- a/gophor.go +++ b/gophor.go @@ -32,8 +32,6 @@ import "C" * Gopher server */ var ( - ServerDir = "" - ServerRoot = flag.String("root", "/var/gopher", "Change server root directory.") ServerHostname = flag.String("hostname", "127.0.0.1", "Change server hostname (FQDN).") ServerPort = flag.Int("port", 70, "Change server port (0 to disable unencrypted traffic).") @@ -41,7 +39,6 @@ var ( // ServerTlsCert = flag.String("cert", "", "Change server TLS/SSL cert file.") ExecAsUid = flag.Int("uid", 1000, "Change UID to drop executable privileges to.") ExecAsGid = flag.Int("gid", 100, "Change GID to drop executable privileges to.") - NoChroot = flag.Bool("no-chroot", false, "Disable using chroot for server root directory.") SystemLog = flag.String("system-log", "", "Change server system log file (blank outputs to stderr).") AccessLog = flag.String("access-log", "", "Change server access log file (blank outputs to stderr).") LoggingType = flag.Int("log-type", 0, "Change server log file handling -- 0:default 1:disable") @@ -63,14 +60,8 @@ func main() { logSystem("Entered server directory: %s\n", *ServerRoot) /* Try enter chroot if requested */ - if !*NoChroot { - chrootServerDir() - logSystem("Chroot success, new root: %s\n", *ServerRoot) - ServerDir = "/" - } else { - logSystem("Flag 'no-chroot', selected\n") - ServerDir = *ServerRoot - } + chrootServerDir() + logSystem("Chroot success, new root: %s\n", *ServerRoot) /* Set-up socket while we still have privileges (if held) */ listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *ServerHostname, *ServerPort)) @@ -98,8 +89,7 @@ func main() { /* Run this in it's own goroutine so we can go straight back to accepting */ go func() { - w := new(Worker) - w.Init(&newConn) + w := NewWorker(&newConn) w.Serve() }() } diff --git a/worker.go b/worker.go index 2d8ff31..0df19c9 100644 --- a/worker.go +++ b/worker.go @@ -17,14 +17,21 @@ const ( MaxSocketReadChunks = 4 FileReadBufSize = 512 GopherMapFile = "/gophermap" + DefaultShell = "/bin/sh" ) type Worker struct { - Socket net.Conn + Socket net.Conn + Hidden map[string]bool } -func (worker *Worker) Init(socket *net.Conn) { +func NewWorker(socket *net.Conn) *Worker { + worker := new(Worker) worker.Socket = *socket + worker.Hidden = map[string]bool{ + "gophermap": true, + } + return worker } func (worker *Worker) Serve() { @@ -72,7 +79,7 @@ func (worker *Worker) Serve() { } /* Respond */ - gophorErr := serverRespond(worker, received) + gophorErr := worker.Respond(received) if gophorErr != nil { logSystemError("%s\n", gophorErr.Error()) } @@ -97,8 +104,29 @@ func (worker *Worker) SendRaw(b []byte) *GophorError { return nil } -func serverRespond(worker *Worker, data []byte) *GophorError { - /* Only read up to first tab / cr-lf */ +func (worker *Worker) Log(format string, args ...interface{}) { + logAccess(worker.Socket.RemoteAddr().String()+" "+format, args...) +} + +func (worker *Worker) LogError(format string, args ...interface{}) { + logAccessError(worker.Socket.RemoteAddr().String()+" "+format, args...) +} + +func (worker *Worker) SanitizePath(dataStr string) string { + /* Clean path and trim '/' prefix if still exists */ + requestPath := strings.TrimPrefix(path.Clean(dataStr), "/") + + /* Naughty directory traversal! Hackers get ROOT */ + if strings.HasPrefix(requestPath, "/") { + worker.LogError("illegal path requested: %s\n", dataStr) + requestPath = "." + } + + return requestPath +} + +func (worker *Worker) Respond(data []byte) *GophorError { + /* Only read up to first tab or cr-lf */ dataStr := "" dataLen := len(data) for i := 0; i < dataLen; i += 1 { @@ -115,25 +143,16 @@ func serverRespond(worker *Worker, data []byte) *GophorError { dataStr += string(data[i]) } - /* Clean path and get shortest possible from current directory */ - requestPath := path.Clean(dataStr) - - /* Even if asking for root, we trim the initial '/' now its been cleaned up */ - requestPath = strings.TrimPrefix(requestPath, "/") - - /* Ensure alway a relative paths + WITHIN ServerDir, serve them root otherwise */ - if strings.HasPrefix(requestPath, "..") { - logAccessError("%s illegal path requested: %s\n", worker.Socket.RemoteAddr(), dataStr) - requestPath = "." - } + /* Sanitize supplied path */ + requestPath := worker.SanitizePath(dataStr) /* Open requestPath */ - fd, err := os.Open(requestPath) + file, err := os.Open(requestPath) if err != nil { - worker.SendErrorType("read fail\n") /* Purposely vague errors */ + worker.SendErrorType("read fail\n") /* Purposely vague errors */ return &GophorError{ FileOpenErr, err } } - defer fd.Close() + defer file.Close() /* Leads to some more concise code below */ type FileType int @@ -146,7 +165,7 @@ func serverRespond(worker *Worker, data []byte) *GophorError { /* If not empty requestPath, check file type */ fileType := Dir if requestPath != "." { - stat, err := fd.Stat() + stat, err := file.Stat() if err != nil { worker.SendErrorType("read fail\n") /* Purposely vague errors */ return &GophorError{ FileStatErr, err } @@ -170,23 +189,23 @@ func serverRespond(worker *Worker, data []byte) *GophorError { case Dir: /* First try to serve gopher map */ requestPath = path.Join(requestPath, GopherMapFile) - fd2, err := os.Open(requestPath) - defer fd2.Close() + mapFile, err := os.Open(requestPath) + defer mapFile.Close() if err == nil { /* Read GopherMapFile contents */ - logAccess("%s serve gophermap: /%s\n", worker.Socket.RemoteAddr(), requestPath) + worker.Log("serve gophermap: /%s\n", requestPath) - response, gophorErr = readGophermap(fd2) + response, gophorErr = worker.ReadGophermap(file, mapFile) if gophorErr != nil { worker.SendErrorType("gophermap read fail\n") return gophorErr } } else { /* Get directory listing */ - logAccess("%s serve dir: /%s\n", worker.Socket.RemoteAddr(), requestPath) + worker.Log("serve dir: /%s\n", requestPath) - response, gophorErr = listDir(fd) + response, gophorErr = worker.ListDir(file) if gophorErr != nil { worker.SendErrorType("dir list fail\n") return gophorErr @@ -199,9 +218,9 @@ func serverRespond(worker *Worker, data []byte) *GophorError { /* Regular file */ case File: /* Read file contents */ - logAccess("%s serve file: /%s\n", worker.Socket.RemoteAddr(), requestPath) + worker.Log("%s serve file: /%s\n", requestPath) - response, gophorErr = readFile(fd) + response, gophorErr = worker.ReadFile(file) if gophorErr != nil { worker.SendErrorText("file read fail\n") return gophorErr @@ -216,11 +235,11 @@ func serverRespond(worker *Worker, data []byte) *GophorError { return worker.SendRaw(response) } -func readGophermap(fd *os.File) ([]byte, *GophorError) { +func (worker *Worker) ReadGophermap(dir, mapFile *os.File) ([]byte, *GophorError) { fileContents := make([]byte, 0) /* Create reader and scanner from this */ - reader := bufio.NewReader(fd) + reader := bufio.NewReader(mapFile) scanner := bufio.NewScanner(reader) /* Setup scanner to split on CrLf */ @@ -240,13 +259,89 @@ func readGophermap(fd *os.File) ([]byte, *GophorError) { }) /* Scan, format each token and add to fileContents */ + doEnd := false for scanner.Scan() { line := scanner.Text() - if len(line) == 0 || !strings.Contains(line, string(Tab)) && line != "." { - line = string(TypeInfo) + line + + /* Parse the line item type and handle */ + lineType := parseLineType(line) + switch lineType { + case TypeInfoNotStated: + /* Append TypeInfo to the beginning of line */ + line = string(TypeInfo)+line+CrLf + + case TypeComment: + /* We ignore this line */ + continue + + case TypeHiddenFile: + /* Add to hidden files map */ + worker.Hidden[line[1:]] = true + + case TypeSubGophermap: + /* Try to read subgophermap of file name */ + line = string(TypeInfo)+"Error: subgophermaps not supported"+CrLf + +/* + subMapFile, err := os.Open(line[1:]) + if err != nil { + worker.LogError("error opening subgophermap: /%s --> %s\n", mapFile.Name(), line[1:]) + line = fmt.Sprintf(string(TypeInfo)+"Error reading subgophermap: %s"+CrLf, line[1:]) + } else { + subMapContent, gophorError := worker.ReadFile(subMapFile) + if gophorError != nil { + worker.LogError("error reading subgophermap: /%s --> %s\n", mapFile.Name(), line[1:]) + line = fmt.Sprintf(string(TypeInfo)+"Error reading subgophermap: %s"+CrLf, line[1:]) + } else { + line = strings.Replace(string(subMapContent), "\n", CrLf, -1) + if !strings.HasSuffix(line, CrLf) { + line += CrLf + } + } + } +*/ + + case TypeExec: + /* Try executing supplied line */ + line = string(TypeInfo)+"Error: inline shell commands not support"+CrLf + +/* + err := exec.Command(line[1:]).Run() + if err != nil { + line = fmt.Sprintf(string(TypeInfo)+"Error executing command: %s"+CrLf, line[1:]) + } else { + line = strings.Replace(string(""), "\n", CrLf, -1) + if !strings.HasSuffix(line, CrLf) { + line += CrLf + } + } +*/ + + case TypeEnd: + /* Lastline, break out at end of loop */ + doEnd = true + line = LastLine + + case TypeEndBeginList: + /* Read current directory listing then break out at end of loop */ + doEnd = true + dirListing, gophorErr := worker.ListDir(dir) + if gophorErr != nil { + return nil, gophorErr + } + line = string(dirListing) + LastLine + + default: + line += CrLf } - line += CrLf + + /* Append generated line to total fileContents */ fileContents = append(fileContents, []byte(line)...) + + /* Break out of read loop if requested */ + if doEnd { + break + } } /* If scanner didn't finish cleanly, return nil and error */ @@ -254,22 +349,27 @@ func readGophermap(fd *os.File) ([]byte, *GophorError) { return nil, &GophorError{ FileReadErr, scanner.Err() } } + /* If we never hit doEnd, append a LastLine ourselves */ + if !doEnd { + fileContents = append(fileContents, []byte(LastLine)...) + } + return fileContents, nil } -func readFile(fd *os.File) ([]byte, *GophorError) { +func (worker *Worker) ReadFile(file *os.File) ([]byte, *GophorError) { var count int fileContents := make([]byte, 0) buf := make([]byte, FileReadBufSize) var err error - reader := bufio.NewReader(fd) + reader := bufio.NewReader(file) for { count, err = reader.Read(buf) if err != nil && err != io.EOF { return nil, &GophorError{ FileReadErr, err } - } + } for i := 0; i < count; i += 1 { if buf[i] == 0 { @@ -287,8 +387,8 @@ func readFile(fd *os.File) ([]byte, *GophorError) { return fileContents, nil } -func listDir(fd *os.File) ([]byte, *GophorError) { - files, err := fd.Readdir(-1) +func (worker *Worker) ListDir(dir *os.File) ([]byte, *GophorError) { + files, err := dir.Readdir(-1) if err != nil { return nil, &GophorError{ DirListErr, err } } @@ -297,8 +397,10 @@ func listDir(fd *os.File) ([]byte, *GophorError) { dirContents := make([]byte, 0) for _, file := range files { - /* Unless specificially compiled not to, we skip hidden files */ - if !ShowHidden && file.Name()[0] == '.' { + /* Skip dotfiles + gophermap file + requested hidden */ + if file.Name()[0] == '.' || file.Name() == "gophermap" { + continue + } else if _, ok := worker.Hidden[file.Name()]; ok { continue } @@ -306,13 +408,13 @@ func listDir(fd *os.File) ([]byte, *GophorError) { switch { case file.Mode() & os.ModeDir != 0: /* Directory -- create directory listing */ - itemPath := path.Join(fd.Name(), file.Name()) + itemPath := path.Join(dir.Name(), file.Name()) entity = newDirEntity(TypeDirectory, file.Name(), "/"+itemPath, *ServerHostname, *ServerPort) dirContents = append(dirContents, entity.Bytes()...) case file.Mode() & os.ModeType == 0: /* Regular file -- find item type and creating listing */ - itemPath := path.Join(fd.Name(), file.Name()) + itemPath := path.Join(dir.Name(), file.Name()) itemType := getItemType(itemPath) entity = newDirEntity(itemType, file.Name(), "/"+itemPath, *ServerHostname, *ServerPort) dirContents = append(dirContents, entity.Bytes()...)