first iteration of code

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
master
kim (grufwub) 4 years ago
commit 28b228472d

@ -0,0 +1 @@
GoLang Gopher Server

@ -0,0 +1,249 @@
package main
import (
"fmt"
"os"
"net"
"bufio"
"path"
)
const (
GopherMapFile = "/gophermap"
)
type Client struct {
Cmd chan Command
Socket net.Conn
}
func (client *Client) Init(conn *net.Conn) {
client.Cmd = make(chan Command)
client.Socket = *conn
}
func (client *Client) Start() {
go func() {
defer func() {
/* Close-up shop */
client.Socket.Close()
close(client.Cmd)
}()
var count int
var err error
/* Read buffer + final result */
b := make([]byte, SocketReadBufSize)
result := make([]byte, 0)
/* Buffered read from listener */
iter := 0
for {
count, err = client.Socket.Read(b)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading from socket %s: %v\n", client.Socket, err)
return
}
result = append(result, b...)
if count != SocketReadBufSize {
/* Reached end of read */
break
}
if iter == MaxSocketReadChunks {
fmt.Fprintf(os.Stderr, "Reached max socket read size: %d. Closing connection...\n", MaxSocketReadChunks*SocketReadBufSize)
return
}
iter += 1
}
fmt.Println("Hostname:", client.Socket.LocalAddr(), "Result:", string(result))
/* Respond */
gophorErr := serverRespond(client, result)
if gophorErr != nil {
fmt.Fprintf(os.Stderr, gophorErr.Error() + "\n")
}
}()
}
func serverRespond(client *Client, data []byte) *GophorError {
path := socketBytesToString(data)
pathLen := len(path)
var response []byte
var gophorErr *GophorError
var err error
if (pathLen == 1 && path == "\r") ||
(pathLen == 2 && path == "\r\n") {
/* Empty line received, treat as dir listing for root */
fd, err := os.Open(GopherMapFile)
defer fd.Close()
if err == nil {
/* Read GopherMapFile contents */
fileContents, gophorErr := readFile(fd)
if gophorErr != nil {
return gophorErr
}
/* Serve GopherMapFile */
count, err := client.Socket.Write(fileContents)
if err != nil {
return &GophorError{ SocketWrite, err }
} else if count != len(fileContents) {
return &GophorError{ SocketWriteCount, nil }
}
} else {
fmt.Fprintf(os.Stderr, "Error reading GopherMapFile, list dir / instead\n")
/* Close fd, re-open directory instead */
fd.Close()
fd, err = os.Open("/")
/* Get directory listing */
response, gophorErr = listDir(fd)
if gophorErr != nil {
return gophorErr
}
}
} else {
fd, err := os.Open(path)
if err != nil {
return &GophorError{ FileOpen, err }
}
defer fd.Close()
stat, err := fd.Stat()
if err != nil {
return &GophorError{ FileStat, err }
}
/* Determine if path or directory */
switch {
/* Directory */
case stat.Mode() & os.ModeDir != 0:
/* First try to serve gopher map */
fd2, err := os.Open(path + GopherMapFile)
defer fd2.Close()
if err == nil {
/* Read GopherMapFile contents */
response, gophorErr = readFile(fd2)
if gophorErr != nil {
return gophorErr
}
} else {
fmt.Fprintf(os.Stderr, "Error reading GopherMapFile, list dir instead: %s\n", path)
/* Get directory listing */
response, gophorErr = listDir(fd)
if gophorErr != nil {
return gophorErr
}
}
/* Regular file */
case stat.Mode() & os.ModeType == 0:
/* Read file contents */
response, gophorErr = readFile(fd)
if gophorErr != nil {
return gophorErr
}
/* Unsupport file type */
default:
return &GophorError{ FileType, nil }
}
}
/* Always finish LastLine bytes */
response = append(response, []byte(LastLine)...)
/* Serve response + always finish with period on a line */
count, err := client.Socket.Write(response)
if err != nil {
return &GophorError{ SocketWrite, err }
} else if count != len(response) {
return &GophorError{ SocketWriteCount, nil }
}
return nil
}
func socketBytesToString(slice []byte) string {
out := ""
/* Use constants here to get that sweet loop-unrolling boost */
for i := 0; i < SocketReadBufSize; i += 1 {
switch slice[i] {
case 0: break
default: out += string(slice[i])
}
}
return out
}
func readFile(fd *os.File) ([]byte, *GophorError) {
var count int
fileContents := make([]byte, FileReadBufSize)
b := make([]byte, FileReadBufSize)
var err error
reader := bufio.NewReader(fd)
for {
count, err = reader.Read(b)
if err != nil {
return nil, &GophorError{ FileRead, err }
} else if count == 0 {
/* Either undocumented error, or reached end of file */
break
}
fileContents = append(fileContents, b...)
if count < FileReadBufSize {
/* EOF */
break
}
}
return fileContents, nil
}
func listDir(fd *os.File) ([]byte, *GophorError) {
files, err := fd.Readdir(-1)
if err != nil {
return nil, &GophorError{ DirList, err }
}
var entity *DirEntity
dirContents := make([]byte, 0)
for _, file := range files {
if !ShowHidden && file.Name()[0] == '.' {
continue
}
switch {
case file.Mode() & os.ModeDir != 0:
/* Directory! */
fullPath := path.Join(fd.Name(), file.Name())
entity = newDirEntity(TypeDirectory, file.Name(), fullPath, *ServerHostname, *ServerPort)
dirContents = append(dirContents, entity.Bytes()...)
case file.Mode() & os.ModeType == 0:
/* Regular file */
fullPath := path.Join(fd.Name(), file.Name())
itemType := getItemType(fullPath)
entity = newDirEntity(itemType, file.Name(), fullPath, *ServerHostname, *ServerPort)
dirContents = append(dirContents, entity.Bytes()...)
default:
/* Ignore */
fmt.Fprintf(os.Stderr, "List dir: skipping file %s of invalid type\n", file.Name())
}
}
return dirContents, nil
}

@ -0,0 +1,164 @@
package main
import (
"strconv"
"strings"
"path/filepath"
)
const (
CrLf = "\r\n"
End = "."
LastLine = End+CrLf
Tab = byte('\t')
)
type ItemType byte
/*
* Item type characters
*/
const (
/* RFC 1436 Standard */
TypeFile = ItemType(0) /* Regular file */
TypeDirectory = ItemType(1) /* Directory */
TypePhonebook = ItemType(2) /* CSO phone-book server */
TypeError = ItemType(3) /* Error */
TypeMacBinHex = ItemType(4) /* Binhexed macintosh file */
TypeDosBinArchive = ItemType(5) /* DOS bin archive, CLIENT MUST READ UNTIL TCP CLOSE */
TypeUnixFile = ItemType(6) /* Unix uuencoded file */
TypeIndexSearch = ItemType(7) /* Index-search server */
TypeTelnet = ItemType(8) /* Text-based telnet session */
TypeBin = ItemType(9) /* Binary file, CLIENT MUST READ UNTIL TCP CLOSE */
TypeTn3270 = ItemType('T') /* Text-based tn3270 session */
TypeGif = ItemType('g') /* Gif format graphics file */
TypeImage = ItemType('I') /* Some kind of image file (client decides how to display) */
TypeRedundant = ItemType('+') /* Redundant server */
/* Non-standard */
TypeInfo = ItemType('i') /* Informational message */
TypeHtml = ItemType('h') /* HTML document */
TypeAudio = ItemType('s') /* Audio file */
TypePng = ItemType('p') /* PNG image */
TypeDoc = ItemType('d') /* Document */
/* Default type */
TypeDefault = TypeBin
)
func (i ItemType) String() string {
switch i {
case TypeFile:
return "TXT"
case TypeDirectory:
return "DIR"
case TypePhonebook:
return "PHO"
case TypeError:
return "ERR"
case TypeMacBinHex:
return "HEX"
case TypeDosBinArchive:
return "ARC"
case TypeUnixFile:
return "UUE"
case TypeIndexSearch:
return "QRY"
case TypeTelnet:
return "TEL"
case TypeBin:
return "BIN"
case TypeTn3270:
return "TN3"
case TypeGif:
return "GIF"
case TypeImage:
return "IMG"
case TypeRedundant:
return "DUP"
case TypeInfo:
return "NFO"
case TypeHtml:
return "HTM"
case TypeAudio:
return "SND"
case TypePng:
return "PNG"
case TypeDoc:
return "DOC"
default:
return "???"
}
}
/*
* Directory Entity data structure for easier handling
*/
type DirEntity struct {
Type ItemType
UserName string
Selector string
Host string
Port string
}
func newDirEntity(t ItemType, name, selector, host string, port int) *DirEntity {
entity := new(DirEntity)
entity.Type = t
entity.UserName = name
if len(selector) > 255 {
selector = selector[:254]
}
entity.Selector = selector
entity.Host = host
entity.Port = strconv.Itoa(port)
return entity
}
func (entity *DirEntity) Bytes() []byte {
b := make([]byte, 0)
b = append(b, byte(entity.Type))
b = append(b, []byte(entity.UserName)...)
b = append(b, Tab)
b = append(b, []byte(entity.Selector)...)
b = append(b, Tab)
b = append(b, []byte(entity.Host)...)
b = append(b, Tab)
b = append(b, []byte(entity.Port)...)
b = append(b, []byte(CrLf)...)
return b
}
var FileExtensions = map[string]ItemType{
".txt": TypeFile,
".gif": TypeGif,
".jpg": TypeImage,
".jpeg": TypeImage,
".png": TypeImage,
".html": TypeHtml,
".ogg": TypeAudio,
".mp3": TypeAudio,
".wav": TypeAudio,
".mod": TypeAudio,
".it": TypeAudio,
".xm": TypeAudio,
".mid": TypeAudio,
".vgm": TypeAudio,
".s": TypeFile,
".c": TypeFile,
".py": TypeFile,
".h": TypeFile,
".md": TypeFile,
".go": TypeFile,
".fs": TypeFile,
}
func getItemType(name string) ItemType {
extension := strings.ToLower(filepath.Ext(name))
fileType, ok := FileExtensions[extension]
if !ok {
return TypeDefault
}
return fileType
}

@ -0,0 +1,66 @@
package main
import (
"fmt"
)
/*
* Client error data structure
*/
type ErrorCode int
const (
/* Filesystem */
FileStat ErrorCode = iota
FileOpen ErrorCode = iota
FileRead ErrorCode = iota
FileType ErrorCode = iota
DirList ErrorCode = iota
/* Sockets */
SocketWrite ErrorCode = iota
SocketWriteCount ErrorCode = iota
/* Parsing */
EmptyItemType ErrorCode = iota
EntityPortParse ErrorCode = iota
)
type GophorError struct {
Code ErrorCode
Err error
}
func (e *GophorError) Error() string {
var str string
switch e.Code {
case FileStat:
str = "file stat fail"
case FileOpen:
str = "file open fail"
case FileRead:
str = "file read fail"
case FileType:
str = "invalid file type"
case DirList:
str = "directory read fail"
case SocketWrite:
str = "socket write fail"
case SocketWriteCount:
str = "socket write count mismatch"
case EmptyItemType:
str = "line string provides no dir entity type"
case EntityPortParse:
str = "parsing dir entity port"
default:
str = "Unknown"
}
if e.Err != nil {
return fmt.Sprintf("GophorError: %s (%s)", str, e.Err.Error())
} else {
return fmt.Sprintf("GophorError: %s", str)
}
}

@ -0,0 +1,97 @@
package main
import (
"fmt"
"os"
"syscall"
"flag"
"net"
)
/*
* Global Constants
*/
const (
ShowHidden = false
SocketReadBufSize = 1024
FileReadBufSize = 1024
MaxSocketReadChunks = 4
)
type Command int
const (
Stop Command = iota
Clean Command = iota
)
/*
* Gopher server
*/
var (
/* Run-time arguments */
ServerRoot = flag.String("root", "/var/gopher", "server root directory")
ServerPort = flag.Int("port", 70, "server listening port")
ServerHostname = flag.String("hostname", "127.0.0.1", "server hostname")
)
func main() {
/* Parse run-time arguments */
flag.Parse()
/* Enter chroot */
if enterChroot() != nil {
os.Exit(1)
}
/* Set-up socket */
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *ServerHostname, *ServerPort))
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening socket on port %d: %v\n", *ServerPort, err)
os.Exit(1)
}
/* Setup client manager */
manager := new(ClientManager)
manager.Init()
manager.Start()
/* Main server loop */
count := 0
for {
newConn, err := listener.Accept()
if err != nil {
fmt.Fprintf(os.Stderr, "Error accepting connection from %s: %v\n", newConn, err)
continue
}
client := new(Client)
client.Init(&newConn)
manager.Register<-client
if count == 5 {
manager.Cmd<-Clean
count = 0
} else {
count += 1
}
}
/* Handle stopping */
}
func enterChroot() error {
err := syscall.Chdir(*ServerRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "Error changing dir to server root %s: %v\n", *ServerRoot, err)
return err
}
err = syscall.Chroot(*ServerRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "Error chroot'ing into server root %s: %v\n", *ServerRoot, err)
return err
}
return nil
}

@ -0,0 +1,59 @@
package main
type ClientManager struct {
Cmd chan Command
Clients map[*Client]bool
Register chan *Client
Unregister chan *Client
}
func (manager *ClientManager) Init() {
manager.Cmd = make(chan Command)
manager.Clients = make(map[*Client]bool)
manager.Register = make(chan *Client)
manager.Unregister = make(chan *Client)
}
func (manager *ClientManager) Start() {
go func() {
for {
select {
case cmd := <-manager.Cmd:
/* Received manager command, handle! */
switch cmd {
case Stop:
/* Stop all clients then delete, break out of run loop */
for client := range manager.Clients {
client.Cmd<-Stop
delete(manager.Clients, client)
}
break
case Clean:
/* Delete all 'done' clients */
for client := range manager.Clients {
select {
case <-client.Cmd:
/* Channel closed, client done, delete! */
delete(manager.Clients, client)
default:
/* Don't lock :) */
}
}
}
case client := <-manager.Register:
/* Received new client to register */
manager.Clients[client] = true
client.Start()
case client := <-manager.Unregister:
/* Received client id to unregister */
if _, ok := manager.Clients[client]; ok {
client.Cmd<-Stop
delete(manager.Clients, client)
}
}
}
}()
}
Loading…
Cancel
Save