Support FastCGI

This commit is contained in:
Trevor Slocum 2020-11-04 12:48:55 -08:00
parent 47450eddfe
commit 5e9515ff09
7 changed files with 240 additions and 48 deletions

View file

@ -74,7 +74,7 @@ Serve static files from specified root directory.
##### Proxy ##### Proxy
Forward request to Gemini server at specified URL. Forward requests to Gemini server at specified URL.
Use the pseudo-scheme `gemini-insecure://` to disable certificate verification. Use the pseudo-scheme `gemini-insecure://` to disable certificate verification.
@ -102,6 +102,18 @@ Request text input from user.
Request sensitive text input from the user. Text will not be shown as it is entered. Request sensitive text input from the user. Text will not be shown as it is entered.
##### Type
Content type is normally detected automatically, defaulting to
`text/gemini; charset=utf-8`. This option forces a specific content type.
##### FastCGI
Forward requests to [FastCGI](https://en.wikipedia.org/wiki/FastCGI) server at
specified address or path.
A `Root` attribute must also be specified to use `FastCGI`.
# Example config.yaml # Example config.yaml
```yaml ```yaml
@ -118,9 +130,13 @@ hosts:
cert: /srv/gemini.rocks/data/cert.crt cert: /srv/gemini.rocks/data/cert.crt
key: /srv/gemini.rocks/data/cert.key key: /srv/gemini.rocks/data/cert.key
paths: paths:
-
path: ^/sites/.*\.php$
root: /home/geminirocks/data
fastcgi: unix:///var/run/php.sock
- -
path: /sites path: /sites
root: /home/geminirocks/data/sites root: /home/geminirocks/data
listdirectory: true listdirectory: true
- -
path: ^/(help|info)$ path: ^/(help|info)$

View file

@ -13,8 +13,10 @@ This page is also available at [gemini://twins.rocketnine.space](gemini://twins.
- Serve static files - Serve static files
- Directory listing (when enabled) - Directory listing (when enabled)
- Serve the output of system commands
- Reverse proxy requests - Reverse proxy requests
- TCP
- [FastCGI](https://en.wikipedia.org/wiki/FastCGI)
- Serve system command output
- Reload configuration on `SIGHUP` - Reload configuration on `SIGHUP`
## Download ## Download
@ -39,7 +41,7 @@ Please share issues and suggestions [here](https://gitlab.com/tslocum/twins/issu
## Dependencies ## Dependencies
- [go-gemini](https://github.com/makeworld-the-better-one/go-gemini) - [filetype](https://github.com/h2non/filetype) - MIME type detection
- [go-shellquote](https://github.com/kballard/go-shellquote) - [gofast](https://github.com/yookoala/gofast) - FastCGI client
- [filetype](https://github.com/h2non/filetype) - [go-shellquote](https://github.com/kballard/go-shellquote) - Shell string quoting
- [yaml](https://github.com/go-yaml/yaml/tree/v3) - [yaml](https://github.com/go-yaml/yaml/tree/v3) - Configuration parsing

View file

@ -5,12 +5,15 @@ import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
"net/url"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/yookoala/gofast"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -32,6 +35,12 @@ type pathConfig struct {
// List directory entries // List directory entries
ListDirectory bool ListDirectory bool
// Content type
Type string
// FastCGI server address
FastCGI string
r *regexp.Regexp r *regexp.Regexp
cmd []string cmd []string
} }
@ -49,8 +58,9 @@ type serverConfig struct {
Hosts map[string]*hostConfig Hosts map[string]*hostConfig
hostname string hostname string
port int port int
fcgiPools map[string]*gofast.ClientPool
} }
var config *serverConfig var config *serverConfig
@ -92,7 +102,8 @@ func readconfig(configPath string) error {
} }
} }
for _, host := range config.Hosts { config.fcgiPools = make(map[string]*gofast.ClientPool)
for hostname, host := range config.Hosts {
if host.Cert == "" || host.Key == "" { if host.Cert == "" || host.Key == "" {
log.Fatal("a certificate must be specified for each domain (gemini requires TLS for all connections)") log.Fatal("a certificate must be specified for each domain (gemini requires TLS for all connections)")
} }
@ -109,7 +120,7 @@ func readconfig(configPath string) error {
} else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) || } else if (serve.Root != "" && (serve.Proxy != "" || serve.Command != "")) ||
(serve.Proxy != "" && (serve.Root != "" || serve.Command != "")) || (serve.Proxy != "" && (serve.Root != "" || serve.Command != "")) ||
(serve.Command != "" && (serve.Root != "" || serve.Proxy != "")) { (serve.Command != "" && (serve.Root != "" || serve.Proxy != "")) {
log.Fatal("only one root, reverse proxy or command may specified in a serve entry") log.Fatal("only one root, proxy or command resource may specified for a path")
} }
if serve.Path[0] == '^' { if serve.Path[0] == '^' {
@ -118,7 +129,22 @@ func readconfig(configPath string) error {
serve.Path = serve.Path[:len(serve.Path)-1] serve.Path = serve.Path[:len(serve.Path)-1]
} }
if serve.Command != "" { if serve.FastCGI != "" {
if serve.Root == "" {
log.Fatalf("root must be specified to use fastcgi resource %s of path %s%s", serve.FastCGI, hostname, serve.Path)
}
if config.fcgiPools[serve.FastCGI] == nil {
f, err := url.Parse(serve.FastCGI)
if err != nil {
log.Fatalf("failed to parse fastcgi resource %s: %s", serve.FastCGI, err)
}
connFactory := gofast.SimpleConnFactory(f.Scheme, f.Host+f.Path)
clientFactory := gofast.SimpleClientFactory(connFactory, 0)
config.fcgiPools[serve.FastCGI] = gofast.NewClientPool(clientFactory, 1, 1*time.Minute)
}
} else if serve.Command != "" {
serve.cmd, err = shellquote.Split(serve.Command) serve.cmd, err = shellquote.Split(serve.Command)
if err != nil { if err != nil {
log.Fatalf("failed to parse command %s: %s", serve.cmd, err) log.Fatalf("failed to parse command %s: %s", serve.cmd, err)

3
go.mod
View file

@ -5,6 +5,7 @@ go 1.15
require ( require (
github.com/h2non/filetype v1.1.0 github.com/h2non/filetype v1.1.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/makeworld-the-better-one/go-gemini v0.9.0 github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107
golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
) )

42
go.sum
View file

@ -1,12 +1,46 @@
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA= github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA=
github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/makeworld-the-better-one/go-gemini v0.9.0 h1:Iz4ywRDrfsyoR8xZOkSKGXXftMR2spIV6ibVuhrKvSw= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/makeworld-the-better-one/go-gemini v0.9.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4= github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107 h1:wfqP/vw5tHVeFQJbnmyXSi7E2ZeshpKhR4kuUR5B7yQ=
github.com/yookoala/gofast v0.4.1-0.20201013050739-975113c54107/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200908211811-12e1bf57a112/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b h1:ILx+nYAUTq4JtXxrXKQ82j7nruEwlXRfUn+kxtsnElg=
golang.org/x/tools v0.0.0-20201104193857-22bd85271a8b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

118
server.go
View file

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"crypto/x509"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -20,10 +21,34 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/h2non/filetype" "github.com/h2non/filetype"
"github.com/makeworld-the-better-one/go-gemini"
) )
const readTimeout = 30 * time.Second const (
readTimeout = 30 * time.Second
urlMaxLength = 1024
)
const (
statusInput = 10
statusSensitiveInput = 11
statusSuccess = 20
statusRedirectTemporary = 30
statusRedirectPermanent = 31
statusTemporaryFailure = 40
statusUnavailable = 41
statusCGIError = 42
statusProxyError = 43
statusPermanentFailure = 50
statusNotFound = 51
statusGone = 52
statusProxyRequestRefused = 53
statusBadRequest = 59
)
func writeHeader(c net.Conn, code int, meta string) { func writeHeader(c net.Conn, code int, meta string) {
fmt.Fprintf(c, "%d %s\r\n", code, meta) fmt.Fprintf(c, "%d %s\r\n", code, meta)
@ -36,15 +61,15 @@ func writeHeader(c net.Conn, code int, meta string) {
func writeStatus(c net.Conn, code int) { func writeStatus(c net.Conn, code int) {
var meta string var meta string
switch code { switch code {
case gemini.StatusTemporaryFailure: case statusTemporaryFailure:
meta = "Temporary failure" meta = "Temporary failure"
case gemini.StatusProxyError: case statusProxyError:
meta = "Proxy error" meta = "Proxy error"
case gemini.StatusBadRequest: case statusBadRequest:
meta = "Bad request" meta = "Bad request"
case gemini.StatusNotFound: case statusNotFound:
meta = "Not found" meta = "Not found"
case gemini.StatusProxyRequestRefused: case statusProxyRequestRefused:
meta = "Proxy request refused" meta = "Proxy request refused"
} }
writeHeader(c, code, meta) writeHeader(c, code, meta)
@ -74,7 +99,7 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
return nil return nil
}) })
if err != nil { if err != nil {
writeStatus(c, gemini.StatusTemporaryFailure) writeStatus(c, statusTemporaryFailure)
return return
} }
// List directories first // List directories first
@ -87,7 +112,7 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
return i < j return i < j
}) })
writeHeader(c, gemini.StatusSuccess, "text/gemini; charset=utf-8") writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
fmt.Fprintf(c, "# %s\r\n", request.Path) fmt.Fprintf(c, "# %s\r\n", request.Path)
if numDirs > 0 || numFiles > 0 { if numDirs > 0 || numFiles > 0 {
@ -141,7 +166,7 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listDir bool) { func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listDir bool) {
fi, err := os.Stat(filePath) fi, err := os.Stat(filePath)
if err != nil { if err != nil {
writeStatus(c, gemini.StatusNotFound) writeStatus(c, statusNotFound)
return return
} }
@ -152,7 +177,7 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
if requestData[len(requestData)-1] != '/' { if requestData[len(requestData)-1] != '/' {
// Add trailing slash // Add trailing slash
log.Println(requestData) log.Println(requestData)
writeHeader(c, gemini.StatusRedirectPermanent, requestData+"/") writeHeader(c, statusRedirectPermanent, requestData+"/")
return return
} }
@ -172,10 +197,10 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
serveDirectory(c, request, originalPath) serveDirectory(c, request, originalPath)
return return
} }
writeStatus(c, gemini.StatusNotFound) writeStatus(c, statusNotFound)
return return
} else if err != nil { } else if err != nil {
writeStatus(c, gemini.StatusTemporaryFailure) writeStatus(c, statusTemporaryFailure)
return return
} }
@ -202,7 +227,7 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
if mimeType == "" { if mimeType == "" {
mimeType = "text/gemini; charset=utf-8" mimeType = "text/gemini; charset=utf-8"
} }
writeHeader(c, gemini.StatusSuccess, mimeType) writeHeader(c, statusSuccess, mimeType)
// Write body // Write body
c.Write(buf[:n]) c.Write(buf[:n])
@ -221,7 +246,7 @@ func serveProxy(c net.Conn, requestData, proxyURL string) {
} }
proxy, err := tls.Dial("tcp", proxyURL, tlsConfig) proxy, err := tls.Dial("tcp", proxyURL, tlsConfig)
if err != nil { if err != nil {
writeStatus(c, gemini.StatusProxyError) writeStatus(c, statusProxyError)
return return
} }
defer proxy.Close() defer proxy.Close()
@ -254,11 +279,11 @@ func serveCommand(c net.Conn, userInput string, command []string) {
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
writeStatus(c, gemini.StatusProxyError) writeStatus(c, statusProxyError)
return return
} }
writeHeader(c, gemini.StatusSuccess, "text/gemini; charset=utf-8") writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
c.Write(buf.Bytes()) c.Write(buf.Bytes())
if verbose { if verbose {
@ -293,7 +318,7 @@ func replaceWithUserInput(command []string, userInput string) []string {
return newCommand return newCommand
} }
func handleConn(c net.Conn) { func handleConn(c *tls.Conn) {
if verbose { if verbose {
t := time.Now() t := time.Now()
defer func() { defer func() {
@ -318,28 +343,39 @@ func handleConn(c net.Conn) {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Println(scanner.Text(), "FAILED") log.Println(scanner.Text(), "FAILED")
writeStatus(c, gemini.StatusBadRequest) writeStatus(c, statusBadRequest)
return return
} }
state := c.ConnectionState()
certs := state.PeerCertificates
var clientCertKeys [][]byte
for _, cert := range certs {
pubKey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
continue
}
clientCertKeys = append(clientCertKeys, pubKey)
}
if verbose { if verbose {
log.Printf("> %s\n", requestData) log.Printf("> %s\n", requestData)
} }
if len(requestData) > gemini.URLMaxLength || !utf8.ValidString(requestData) { if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) {
writeStatus(c, gemini.StatusBadRequest) writeStatus(c, statusBadRequest)
return return
} }
request, err := url.Parse(requestData) request, err := url.Parse(requestData)
if err != nil { if err != nil {
writeStatus(c, gemini.StatusBadRequest) writeStatus(c, statusBadRequest)
return return
} }
requestHostname := request.Hostname() requestHostname := request.Hostname()
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') { if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
writeStatus(c, gemini.StatusBadRequest) writeStatus(c, statusBadRequest)
return return
} }
@ -355,12 +391,12 @@ func handleConn(c net.Conn) {
request.Scheme = "gemini" request.Scheme = "gemini"
} }
if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) { if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) {
writeStatus(c, gemini.StatusProxyRequestRefused) writeStatus(c, statusProxyRequestRefused)
} }
if request.Path == "" { if request.Path == "" {
// Redirect to / // Redirect to /
writeHeader(c, gemini.StatusRedirectPermanent, requestData+"/") writeHeader(c, statusRedirectPermanent, requestData+"/")
return return
} }
@ -371,7 +407,7 @@ func handleConn(c net.Conn) {
} }
requestQuery, err := url.QueryUnescape(request.RawQuery) requestQuery, err := url.QueryUnescape(request.RawQuery)
if err != nil { if err != nil {
writeStatus(c, gemini.StatusBadRequest) writeStatus(c, statusBadRequest)
return return
} }
@ -392,10 +428,10 @@ func handleConn(c net.Conn) {
requireInput := serve.Input != "" || serve.SensitiveInput != "" requireInput := serve.Input != "" || serve.SensitiveInput != ""
if requestQuery == "" && requireInput { if requestQuery == "" && requireInput {
if serve.Input != "" { if serve.Input != "" {
writeHeader(c, gemini.StatusInput, serve.Input) writeHeader(c, statusInput, serve.Input)
return return
} else if serve.SensitiveInput != "" { } else if serve.SensitiveInput != "" {
writeHeader(c, gemini.StatusSensitiveInput, serve.SensitiveInput) writeHeader(c, statusSensitiveInput, serve.SensitiveInput)
return return
} }
} }
@ -404,6 +440,16 @@ func handleConn(c net.Conn) {
if serve.Proxy != "" { if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy) serveProxy(c, requestData, serve.Proxy)
return return
} else if serve.FastCGI != "" {
contentType := "text/gemini; charset=utf-8"
if serve.Type != "" {
contentType = serve.Type
}
writeHeader(c, statusSuccess, contentType)
filePath := path.Join(serve.Root, request.Path[1:])
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
return
} else if serve.cmd != nil { } else if serve.cmd != nil {
if requireInput { if requireInput {
newCommand := replaceWithUserInput(serve.cmd, requestQuery) newCommand := replaceWithUserInput(serve.cmd, requestQuery)
@ -421,6 +467,16 @@ func handleConn(c net.Conn) {
if serve.Proxy != "" { if serve.Proxy != "" {
serveProxy(c, requestData, serve.Proxy) serveProxy(c, requestData, serve.Proxy)
return return
} else if serve.FastCGI != "" {
contentType := "text/gemini; charset=utf-8"
if serve.Type != "" {
contentType = serve.Type
}
writeHeader(c, statusSuccess, contentType)
filePath := path.Join(serve.Root, request.Path[1:])
serveFastCGI(c, config.fcgiPools[serve.FastCGI], request, filePath)
return
} else if serve.cmd != nil { } else if serve.cmd != nil {
if requireInput { if requireInput {
newCommand := replaceWithUserInput(serve.cmd, requestQuery) newCommand := replaceWithUserInput(serve.cmd, requestQuery)
@ -444,9 +500,9 @@ func handleConn(c net.Conn) {
} }
if matchedHost { if matchedHost {
writeStatus(c, gemini.StatusNotFound) writeStatus(c, statusNotFound)
} else { } else {
writeStatus(c, gemini.StatusProxyRequestRefused) writeStatus(c, statusProxyRequestRefused)
} }
} }
@ -468,7 +524,7 @@ func handleListener(l net.Listener) {
log.Fatal(err) log.Fatal(err)
} }
go handleConn(conn) go handleConn(conn.(*tls.Conn))
} }
} }

57
server_fcgi.go Normal file
View file

@ -0,0 +1,57 @@
package main
import (
"bytes"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"github.com/yookoala/gofast"
)
type responseWriter struct {
io.WriteCloser
header http.Header
}
func newResponseWriter(out io.WriteCloser) *responseWriter {
return &responseWriter{
WriteCloser: out,
header: make(http.Header),
}
}
func (w *responseWriter) Header() http.Header {
return w.header
}
func (w *responseWriter) WriteHeader(statusCode int) {
// Do nothing
}
func serveFastCGI(c net.Conn, clientPool *gofast.ClientPool, reqURL *url.URL, filePath string) {
r := &http.Request{
Method: "GET",
URL: reqURL,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: ioutil.NopCloser(bytes.NewReader(nil)),
Host: reqURL.Host,
}
gofast.
NewHandler(
gofast.NewFileEndpoint(filePath)(gofast.BasicSession),
clientPool.CreateClient,
).
ServeHTTP(newResponseWriter(c), r)
if verbose {
log.Printf("< exec %s\n", filePath)
}
}