add initial support for gemini, version bump to v3.0.0-alpha
Signed-off-by: kim (grufwub) <grufwub@gmail.com>development
parent
4401fa0499
commit
ca81ea0e6e
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gophi/gemini"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gemini.Run()
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"gophi/core"
|
||||
|
||||
"github.com/grufwub/go-errors"
|
||||
)
|
||||
|
||||
// Gemini specific base errors
|
||||
var (
|
||||
invalidTLSConfigErr = errors.NewBaseError(invalidTLSConfigErrStr)
|
||||
invalidProtocolErr = errors.NewBaseError("")
|
||||
invalidHostPortErr = errors.NewBaseError("")
|
||||
)
|
||||
|
||||
// generateErrorResponse takes an error code and generates an error response byte slice
|
||||
func generateErrorResponse(err *errors.Error) ([]byte, bool) {
|
||||
switch {
|
||||
case err.EqualsBase(core.ConnWriteErr):
|
||||
return nil, false // no point responding if we couldn't write
|
||||
case err.EqualsBase(core.ConnReadErr):
|
||||
return buildErrorResponse("40", statusMeta40), true
|
||||
case err.EqualsBase(core.ConnCloseErr):
|
||||
return nil, false // no point responding if we couldn't close
|
||||
case err.EqualsBase(core.MutexUpgradeErr):
|
||||
return buildErrorResponse("40", statusMeta40), true
|
||||
case err.EqualsBase(core.MutexDowngradeErr):
|
||||
return buildErrorResponse("40", statusMeta40), true
|
||||
case err.EqualsBase(core.FileOpenErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.FileStatErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.FileReadErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.FileTypeErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.DirectoryReadErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.RestrictedPathErr):
|
||||
return buildErrorResponse("51", statusMeta51), true
|
||||
case err.EqualsBase(core.InvalidRequestErr):
|
||||
return buildErrorResponse("59", statusMeta59), true
|
||||
case err.EqualsBase(core.CGIStartErr):
|
||||
return buildErrorResponse("42", statusMeta42), true
|
||||
case err.EqualsBase(core.CGIExitCodeErr):
|
||||
return buildErrorResponse("42", statusMeta42), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func buildErrorResponse(statusCode, statusMeta string) []byte {
|
||||
return append(buildResponseHeader(statusCode, statusMeta), []byte("\n")...)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"gophi/core"
|
||||
"os"
|
||||
|
||||
"github.com/grufwub/go-errors"
|
||||
)
|
||||
|
||||
type headerPlusFileContent struct {
|
||||
contents []byte
|
||||
}
|
||||
|
||||
// WriteToClient writes the current contents of FileContents to the client
|
||||
func (fc *headerPlusFileContent) WriteToClient(client *core.Client, p *core.Path) *errors.Error {
|
||||
return client.Conn().Write(fc.contents)
|
||||
}
|
||||
|
||||
// Load takes an open FD and loads the file contents into FileContents memory
|
||||
func (fc *headerPlusFileContent) Load(p *core.Path, file *os.File) *errors.Error {
|
||||
// Read the file contents
|
||||
contents, err := core.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set sucess header + mime type response header
|
||||
header := buildResponseHeader("20", getFileStatusMeta(p))
|
||||
|
||||
// Set the store contents and return ok
|
||||
fc.contents = append(header, contents...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear empties currently cached FileContents memory
|
||||
func (fc *headerPlusFileContent) Clear() {
|
||||
fc.contents = nil
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"gophi/core"
|
||||
"mime"
|
||||
"path"
|
||||
)
|
||||
|
||||
const gemMimeType = "text/gemini"
|
||||
|
||||
type lineType uint8
|
||||
|
||||
func getFileStatusMeta(p *core.Path) string {
|
||||
// if this is a gem, return this
|
||||
if isGem(p) {
|
||||
return gemMimeType
|
||||
}
|
||||
|
||||
// Get the file extension
|
||||
ext := path.Ext(p.Relative())
|
||||
|
||||
// Calculate mime type for extension
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
|
||||
func buildResponseHeader(statusCode, statusMeta string) []byte {
|
||||
return []byte(statusCode + " " + statusMeta + "\r\n")
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"gophi/core"
|
||||
"io"
|
||||
|
||||
"github.com/grufwub/go-errors"
|
||||
"github.com/grufwub/go-logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// As part of init perform initial entropy assertion
|
||||
b := make([]byte, 1)
|
||||
_, err := io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
logger.Fatal(entropyAssertFailStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Run does as says :)
|
||||
func Run() {
|
||||
// Parse gemini specific flags, then all
|
||||
certFile := flag.String(certFileFlagStr, "", certFileDescStr)
|
||||
keyFile := flag.String(keyFileFlagStr, "", keyFileDescStr)
|
||||
core.ParseFlagsAndSetup(
|
||||
"gemini",
|
||||
1965,
|
||||
func() (*core.Listener, *errors.Error) {
|
||||
// Load the supplied key pair
|
||||
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(invalidTLSConfigErr, err)
|
||||
}
|
||||
|
||||
// Create TLS config
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
config.Rand = rand.Reader
|
||||
|
||||
// Create listener!
|
||||
l, err := tls.Listen("tcp", core.Bind+":"+core.Port, config)
|
||||
if err != nil {
|
||||
return nil, errors.WrapError(core.ListenerBeginErr, err)
|
||||
}
|
||||
|
||||
// Return wrapper listener
|
||||
return core.NewListener(l), nil
|
||||
},
|
||||
newFileContent,
|
||||
handleDirectory,
|
||||
handleLargeFile,
|
||||
)
|
||||
|
||||
// Start!
|
||||
core.Start(serve)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"gophi/core"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// gemRegex is the precompiled gemini file name regex check
|
||||
gemRegex = regexp.MustCompile(`^(|.+/|.+\.)gmi$`)
|
||||
)
|
||||
|
||||
// isGem checks against gemini regex as to whether a file path is a gemini file
|
||||
func isGem(path *core.Path) bool {
|
||||
return gemRegex.MatchString(path.Relative())
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"gophi/core"
|
||||
"os"
|
||||
|
||||
"github.com/grufwub/go-errors"
|
||||
)
|
||||
|
||||
// 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(clientReadFailStr)
|
||||
handleError(client, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Strip everything up to first '/'
|
||||
proto, line := core.SplitBy(string(received), "://")
|
||||
|
||||
// If line is empty, means none supplied --> assume gemini.
|
||||
// Else, we check for valid proto
|
||||
if line == "" {
|
||||
line = proto
|
||||
} else if proto != "gemini" {
|
||||
client.LogError(clientRequestParseFailStr)
|
||||
handleError(client, errors.NewError(core.ConnWriteErr))
|
||||
return
|
||||
}
|
||||
|
||||
// Split up to first '/', ensure first part contains expected
|
||||
// host:port combo
|
||||
hostPort, line := core.SplitBy(line, "/")
|
||||
host, port := core.SplitBy(hostPort, ":")
|
||||
if host != core.Hostname || port != core.Port {
|
||||
client.LogError(clientRequestParseFailStr)
|
||||
handleError(client, errors.NewError(core.ConnWriteErr))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse new request
|
||||
request, err := core.ParseURLEncodedRequest(line)
|
||||
if err != nil {
|
||||
client.LogError(clientRequestParseFailStr)
|
||||
handleError(client, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the request!
|
||||
err = core.HandleClient(
|
||||
// Current client
|
||||
client,
|
||||
|
||||
// Current request
|
||||
request,
|
||||
)
|
||||
|
||||
// Final error handling
|
||||
if err != nil {
|
||||
handleError(client, err)
|
||||
client.LogError(clientServeFailStr, request.String())
|
||||
} else {
|
||||
client.LogInfo(clientServedStr, request.String())
|
||||
}
|
||||
}
|
||||
|
||||
// handleError determines whether to send an error response to the client, and logs to system
|
||||
func handleError(client *core.Client, err *errors.Error) {
|
||||
response, ok := generateErrorResponse(err)
|
||||
if ok {
|
||||
client.Conn().Write(response)
|
||||
}
|
||||
core.SystemLog.Errorf(err.Error())
|
||||
}
|
||||
|
||||
func handleDirectory(client *core.Client, fd *os.File, p *core.Path) *errors.Error {
|
||||
// First check for index gem, create gem Path object
|
||||
indexGem := p.JoinPath("index.gmi")
|
||||
|
||||
// If index gem exists, we fetch this
|
||||
fd2, err := core.OpenFile(indexGem)
|
||||
if err == nil {
|
||||
stat, osErr := fd2.Stat()
|
||||
if osErr == nil {
|
||||
// Fetch gem and defer close
|
||||
defer fd2.Close()
|
||||
return core.FetchFile(client, fd2, stat, indexGem)
|
||||
}
|
||||
|
||||
// Else, just close fd2
|
||||
fd2.Close()
|
||||
}
|
||||
|
||||
// Directory page
|
||||
dirContents := ""
|
||||
|
||||
// Add directory heading + empty line
|
||||
dirContents += "# [ " + core.Hostname + p.Selector() + " ]\n"
|
||||
dirContents += "\n"
|
||||
|
||||
// Scan directory and build lines
|
||||
err = core.ScanDirectory(
|
||||
// Directory fd
|
||||
fd,
|
||||
|
||||
// Directory path
|
||||
p,
|
||||
|
||||
// Iter function
|
||||
func(file os.FileInfo, fp *core.Path) {
|
||||
// Append new formatted file listing
|
||||
dirContents += "=> " + fp.Selector() + " * " + fp.Selector() + "\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) *errors.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{}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package gemini
|
||||
|
||||
// Client error response strings
|
||||
const (
|
||||
statusMeta10 = "Input"
|
||||
statusMeta11 = "Sensitive Input"
|
||||
statusMeta30 = "Temporary Redirect"
|
||||
statusMeta31 = "Permanent Redirect"
|
||||
statusMeta40 = "Temporary Failure"
|
||||
statusMeta41 = "Server Unavailable"
|
||||
statusMeta42 = "CGI Error"
|
||||
statusMeta43 = "Proxy Error"
|
||||
statusMeta44 = "Slow Down"
|
||||
statusMeta50 = "Permanent Failure"
|
||||
statusMeta51 = "Not Found"
|
||||
statusMeta52 = "Gone"
|
||||
statusMeta53 = "Proxy Request Refused"
|
||||
statusMeta59 = "Bad Request"
|
||||
statusMeta60 = "Client Certificate Required"
|
||||
statusMeta61 = "Client Certificate Not Authorised"
|
||||
statusMeta62 = "Certificate Not Valid"
|
||||
)
|
||||
|
||||
// Gemini specific error string constants
|
||||
const (
|
||||
invalidTLSConfigErrStr = "Invalid TLS cert or key file"
|
||||
inavlidProtocolErrStr = "Invalid request protocol"
|
||||
invalidHostPortErrStr = "Invalid host:port pair (not equal to runtime values)"
|
||||
)
|
||||
|
||||
// Gemini flag string constants
|
||||
const (
|
||||
certFileFlagStr = "tls-cert"
|
||||
certFileDescStr = "TLS certificate file (REQUIRED)"
|
||||
|
||||
keyFileFlagStr = "tls-key"
|
||||
keyFileDescStr = "TLS certificate file (REQUIRED)"
|
||||
)
|
||||
|
||||
// Log string constants
|
||||
const (
|
||||
entropyAssertFailStr = "Failed to assert safe source of system entropy exists!"
|
||||
clientReadFailStr = "Failed to read"
|
||||
clientInvalidRequestStr = "Invalid request received"
|
||||
clientRequestParseFailStr = "Failed to parse request"
|
||||
clientServeFailStr = "Failed to serve: %s"
|
||||
clientServedStr = "Served: %s"
|
||||
)
|
Loading…
Reference in New Issue