commit
28b228472d
@ -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…
Reference in New Issue