You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gophi/gemini/server.go

176 lines
4.5 KiB
Go

package gemini
import (
"gophi/core"
"os"
"strings"
)
// rootRedirectHeader stores the root redirect header byte slice,
// because there is no need in recalculating it every time it's needed
var rootRedirect []byte
// serve is the global gemini server's serve function
func serve(client *core.Client) {
// Receive line from client
received, err := client.Conn().ReadLine()
if err != nil {
client.LogError("Conn read fail")
handleError(client, err)
return
}
raw := string(received)
// Ensure is a valid URL string
if core.HasAsciiControlBytes(raw) {
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("Invalid request: %s", raw)
handleError(client, err)
return
}
// Infer no schema as 'gemini', else check we
// were explicitly provided 'gemini'
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
host, path := core.SplitByBefore(strings.TrimPrefix(path, "//"), "/")
// Parse the URL encoded host info
host, port, err := core.ParseEncodedHost(host)
if err != nil {
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("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("Invalid request: %s", raw)
handleError(client, err)
return
}
// Redirect empty path to root
if len(path) < 1 {
client.LogInfo("Redirect to: /")
client.Conn().Write(rootRedirect)
return
}
// Build new Request from raw path and query
request := core.NewRequest(core.BuildPath(path), query)
// Handle the request! And finally, error
err = core.HandleClient(client, request)
if err != nil {
handleError(client, err)
client.LogError("Failed to serve: %s", request.String())
} else {
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 error) {
response, ok := generateErrorResponse(err)
if ok {
client.Conn().Write(response)
}
core.SystemLog.Error(err.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
file2, err := core.OpenFile(indexGem)
if err == nil {
stat, err := file2.Stat()
if err == nil {
// Fetch gem and defer close
defer file2.Close()
return core.FetchFile(client, file2, stat, indexGem)
}
// Else, just close fd2
file2.Close()
}
// 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, []byte("["+core.Hostname+p.Selector()+"]\n\n")...)
dirContents = append(dirContents, []byte("=> "+dirSel+" ..\n")...)
// Scan directory and build lines
err = core.ScanDirectory(
file,
p,
func(file os.FileInfo, fp *core.Path) {
// Calculate escaped selector path
sel := core.EscapePath(fp.Selector())
// If it's a dir, append final '/' to selector
if file.IsDir() {
sel += "/"
}
// Append new formatted file listing
dirContents = append(dirContents, []byte("=> "+sel+" "+file.Name()+"\n")...)
},
)
if err != nil {
return err
}
// Generate gem file header
header := buildResponseHeader("20", gemMimeType)
// Write contents!
return client.Conn().Write(append(header, []byte(dirContents)...))
}
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) error {
// Build the response header
header := buildResponseHeader("20", getFileStatusMeta(p))
// Write the initial header (or return!)
err := client.Conn().Write(header)
if err != nil {
return err
}
// Finally write directly from file
return client.Conn().ReadFrom(file)
}
// newFileContents returns a new FileContents object
func newFileContent(p *core.Path) core.FileContent {
return &headerPlusFileContent{}
}