add initial support for gemini, version bump to v3.0.0-alpha

Signed-off-by: kim (grufwub) <grufwub@gmail.com>
development
kim (grufwub) 4 years ago
parent 4401fa0499
commit ca81ea0e6e

@ -0,0 +1,9 @@
package main
import (
"gophi/gemini"
)
func main() {
gemini.Run()
}

@ -13,7 +13,7 @@ type Client struct {
}
// NewClient returns a new client based on supplied net.TCPConn
func NewClient(conn *net.TCPConn) *Client {
func NewClient(conn net.Conn) *Client {
addr, _ := conn.RemoteAddr().(*net.TCPAddr)
ip, port := &addr.IP, strconv.Itoa(addr.Port)
return &Client{

@ -19,7 +19,7 @@ import (
)
// ParseFlagsAndSetup parses necessary core server flags, and sets up the core ready for Start() to be called
func ParseFlagsAndSetup(proto string) {
func ParseFlagsAndSetup(proto string, defaultPort uint, newListener func() (*Listener, *errors.Error), fileContent func(*Path) FileContent, dirHandler func(*Client, *os.File, *Path) *errors.Error, largeHandler func(*Client, *os.File, *Path) *errors.Error) {
// Setup numerous temporary flag variables, and store the rest
// directly in their final operating location. Strings are stored
// in `string_constants.go` to allow for later localization
@ -28,7 +28,7 @@ func ParseFlagsAndSetup(proto string) {
flag.StringVar(&Root, rootFlagStr, "/var/gopher", rootDescStr)
flag.StringVar(&Bind, bindFlagStr, "", bindDescStr)
flag.StringVar(&Hostname, hostnameFlagStr, "localhost", hostnameDescStr)
port := flag.Uint(portFlagStr, 70, portDescStr)
port := flag.Uint(portFlagStr, defaultPort, portDescStr)
fwdPort := flag.Uint(fwdPortFlagStr, 0, fwdPortDescStr)
flag.DurationVar(&connReadDeadline, connReadTimeoutFlagStr, time.Duration(time.Second*5), connReadTimeoutDescStr)
flag.DurationVar(&connWriteDeadline, connWriteTimeoutFlagStr, time.Duration(time.Second*15), connWriteTimeoutDescStr)
@ -92,7 +92,7 @@ func ParseFlagsAndSetup(proto string) {
// Setup listener
var err *errors.Error
serverListener, err = newListener(Bind, Port)
serverListener, err = newListener()
if err != nil {
SystemLog.Fatalf(listenerBeginFailStr, protocol, Hostname, FwdPort, Bind, Port, err.Error())
}
@ -168,6 +168,11 @@ func ParseFlagsAndSetup(proto string) {
}
}
// Set provided client filesystem handler functions
newFileContent = fileContent
handleDirectory = dirHandler
handleLargeFile = largeHandler
// Setup signal channel
sigChannel = make(chan os.Signal)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)

@ -6,31 +6,19 @@ import (
"github.com/grufwub/go-errors"
)
// listener wraps a net.TCPListener to return our own clients on each Accept()
type listener struct {
l *net.TCPListener
// Listener wraps a net.Listener to return our own clients on each Accept()
type Listener struct {
l net.Listener
}
// NewListener returns a new Listener or Error
func newListener(ip, port string) (*listener, *errors.Error) {
// Try resolve provided ip and port details
laddr, err := net.ResolveTCPAddr("tcp", ip+":"+port)
if err != nil {
return nil, errors.WrapError(ListenerResolveErr, err)
}
// Create listener!
l, err := net.ListenTCP("tcp", laddr)
if err != nil {
return nil, errors.WrapError(ListenerBeginErr, err)
}
return &listener{l}, nil
// NewListener returns a new Listener object wrapping a net.Listener
func NewListener(l net.Listener) *Listener {
return &Listener{l}
}
// Accept accepts a new connection and returns a client, or error
func (l *listener) Accept() (*Client, *errors.Error) {
conn, err := l.l.AcceptTCP()
func (l *Listener) Accept() (*Client, *errors.Error) {
conn, err := l.l.Accept()
if err != nil {
return nil, errors.WrapError(ListenerAcceptErr, err)
}

@ -13,7 +13,7 @@ import (
const (
// Version holds the current version string
Version = "v2.5.8-beta"
Version = "v3.0.0-alpha"
)
var (
@ -47,7 +47,7 @@ var (
userDir string
// Global listener
serverListener *listener
serverListener *Listener
// Client connection related globals
connReadDeadline time.Duration
@ -90,6 +90,11 @@ var (
// MapSCGIRequest is the global function to map a request to SCGI
MapSCGIRequest func(*Request) bool
// Global client-handling filesystem functions
newFileContent func(*Path) FileContent
handleDirectory func(*Client, *os.File, *Path) *errors.Error
handleLargeFile func(*Client, *os.File, *Path) *errors.Error
)
// Start begins operation of the server
@ -122,7 +127,7 @@ func Start(serve func(*Client)) {
}
// HandleClient handles a Client, attempting to serve their request from the filesystem whether a regular file, gophermap, dir listing or CGI script
func HandleClient(client *Client, request *Request, newFileContents func(*Path) FileContent, handleDirectory func(*Client, *os.File, *Path) *errors.Error) *errors.Error {
func HandleClient(client *Client, request *Request) *errors.Error {
// If restricted, return error
if IsRestrictedPath(request.Path()) {
return errors.NewError(RestrictedPathErr)
@ -179,7 +184,7 @@ func HandleClient(client *Client, request *Request, newFileContents func(*Path)
}
// Else just fetch
return FetchFile(client, file, stat, request.Path(), newFileContents)
return FetchFile(client, file, stat, request.Path())
// Unsupported type
default:
@ -188,10 +193,10 @@ func HandleClient(client *Client, request *Request, newFileContents func(*Path)
}
// FetchFile attempts to fetch a file from the cache, using the supplied file stat, Path and serving client. Returns Error status
func FetchFile(client *Client, file *os.File, stat os.FileInfo, p *Path, newFileContent func(*Path) FileContent) *errors.Error {
func FetchFile(client *Client, file *os.File, stat os.FileInfo, p *Path) *errors.Error {
// If file too big, write direct to client
if stat.Size() > fileSizeMax {
return client.Conn().ReadFrom(file)
return handleLargeFile(client, file, p)
}
// Get cache read lock

@ -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"
)

@ -22,16 +22,6 @@ func generateErrorResponse(err *errors.Error) ([]byte, bool) {
return buildErrorLine(errorResponse503), true
case err.EqualsBase(core.ConnCloseErr):
return nil, false // no point responding if we couldn't close
case err.EqualsBase(core.ListenerResolveErr):
return nil, false // not user facing
case err.EqualsBase(core.ListenerBeginErr):
return nil, false // not user facing
case err.EqualsBase(core.ListenerAcceptErr):
return nil, false // not user facing
case err.EqualsBase(core.InvalidIPErr):
return nil, false // not user facing
case err.EqualsBase(core.InvalidPortErr):
return nil, false // not user facing
case err.EqualsBase(core.MutexUpgradeErr):
return buildErrorLine(errorResponse500), true
case err.EqualsBase(core.MutexDowngradeErr):

@ -3,6 +3,9 @@ package gopher
import (
"flag"
"gophi/core"
"net"
"github.com/grufwub/go-errors"
)
// Run does as says :)
@ -14,7 +17,20 @@ func Run() {
admin := flag.String(adminFlagStr, "", adminDescStr)
desc := flag.String(descFlagStr, "", descDescStr)
geo := flag.String(geoFlagStr, "", geoDescStr)
core.ParseFlagsAndSetup("gopher")
core.ParseFlagsAndSetup(
"gopher",
70,
func() (*core.Listener, *errors.Error) {
l, err := net.Listen("tcp", core.Bind+":"+core.Port)
if err != nil {
return nil, errors.WrapError(core.ListenerBeginErr, err)
}
return core.NewListener(l), nil
},
newFileContent,
handleDirectory,
handleLargeFile,
)
// Setup gopher specific global variables
subgophermapSizeMax = int64(1048576.0 * *subgopherSizeMax) // convert float to megabytes

@ -60,59 +60,6 @@ func serve(client *core.Client) {
// Current request
request,
// New file contents function
newFileContent,
// Handle directory function
func(client *core.Client, file *os.File, p *core.Path) *errors.Error {
// First check for gophermap, create gophermap Path object
gophermap := p.JoinPath("gophermap")
// If gophermap exists, we fetch this
file2, err := core.OpenFile(gophermap)
if err == nil {
stat, osErr := file2.Stat()
if osErr == nil {
// Fetch gophermap and defer close
defer file2.Close()
return core.FetchFile(client, file2, stat, gophermap, newFileContent)
}
// Else, just close file2
file2.Close()
}
// Slice to write
dirContents := make([]byte, 0)
// Add directory heading, empty line and a back line
dirContents = append(dirContents, buildLine(typeInfo, "[ "+core.Hostname+p.Selector()+" ]", "TITLE", nullHost, nullPort)...)
dirContents = append(dirContents, buildInfoLine("")...)
dirContents = append(dirContents, buildLine(typeDirectory, "..", p.RelativeDir(), core.Hostname, core.Port)...)
// Scan directory and build lines
err = core.ScanDirectory(
// Directory file
file,
// Directory path
p,
// Iter function
func(file os.FileInfo, fp *core.Path) {
// Append new formatted file listing (if correct type)
dirContents = appendFileListing(dirContents, file, fp)
},
)
if err != nil {
return err
}
// Add footer, write contents
dirContents = append(dirContents, footer...)
return client.Conn().Write(dirContents)
},
)
// Final error handling
@ -124,6 +71,59 @@ func serve(client *core.Client) {
}
}
func handleDirectory(client *core.Client, file *os.File, p *core.Path) *errors.Error {
// First check for gophermap, create gophermap Path object
gophermap := p.JoinPath("gophermap")
// If gophermap exists, we fetch this
file2, err := core.OpenFile(gophermap)
if err == nil {
stat, osErr := file2.Stat()
if osErr == nil {
// Fetch gophermap and defer close
defer file2.Close()
return core.FetchFile(client, file2, stat, gophermap)
}
// Else, just close file2
file2.Close()
}
// Slice to write
dirContents := make([]byte, 0)
// Add directory heading, empty line and a back line
dirContents = append(dirContents, buildLine(typeInfo, "[ "+core.Hostname+p.Selector()+" ]", "TITLE", nullHost, nullPort)...)
dirContents = append(dirContents, buildInfoLine("")...)
dirContents = append(dirContents, buildLine(typeDirectory, "..", p.RelativeDir(), core.Hostname, core.Port)...)
// Scan directory and build lines
err = core.ScanDirectory(
// Directory file
file,
// Directory path
p,
// Iter function
func(file os.FileInfo, fp *core.Path) {
// Append new formatted file listing (if correct type)
dirContents = appendFileListing(dirContents, file, fp)
},
)
if err != nil {
return err
}
// Add footer, write contents
dirContents = append(dirContents, footer...)
return client.Conn().Write(dirContents)
}
func handleLargeFile(client *core.Client, file *os.File, p *core.Path) *errors.Error {
return client.Conn().ReadFrom(file)
}
// 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)

Loading…
Cancel
Save