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.
176 lines
4.5 KiB
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{}
|
|
}
|