big code refactoring + work on supporting new item types

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
This commit is contained in:
kim (grufwub) 2020-04-12 21:32:09 +01:00
parent f736bcae99
commit 352bcb67f6
3 changed files with 205 additions and 56 deletions

View File

@ -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])
}

View File

@ -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()
}()
}

186
worker.go
View File

@ -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()...)