mirror of
https://code.rocketnine.space/tslocum/twins.git
synced 2024-11-27 11:38:14 +01:00
Support FastCGI
This commit is contained in:
parent
47450eddfe
commit
5e9515ff09
7 changed files with 240 additions and 48 deletions
|
@ -74,7 +74,7 @@ Serve static files from specified root directory.
|
|||
|
||||
##### 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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
##### 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
|
||||
|
||||
```yaml
|
||||
|
@ -118,9 +130,13 @@ hosts:
|
|||
cert: /srv/gemini.rocks/data/cert.crt
|
||||
key: /srv/gemini.rocks/data/cert.key
|
||||
paths:
|
||||
-
|
||||
path: ^/sites/.*\.php$
|
||||
root: /home/geminirocks/data
|
||||
fastcgi: unix:///var/run/php.sock
|
||||
-
|
||||
path: /sites
|
||||
root: /home/geminirocks/data/sites
|
||||
root: /home/geminirocks/data
|
||||
listdirectory: true
|
||||
-
|
||||
path: ^/(help|info)$
|
||||
|
|
12
README.md
12
README.md
|
@ -13,8 +13,10 @@ This page is also available at [gemini://twins.rocketnine.space](gemini://twins.
|
|||
|
||||
- Serve static files
|
||||
- Directory listing (when enabled)
|
||||
- Serve the output of system commands
|
||||
- Reverse proxy requests
|
||||
- TCP
|
||||
- [FastCGI](https://en.wikipedia.org/wiki/FastCGI)
|
||||
- Serve system command output
|
||||
- Reload configuration on `SIGHUP`
|
||||
|
||||
## Download
|
||||
|
@ -39,7 +41,7 @@ Please share issues and suggestions [here](https://gitlab.com/tslocum/twins/issu
|
|||
|
||||
## Dependencies
|
||||
|
||||
- [go-gemini](https://github.com/makeworld-the-better-one/go-gemini)
|
||||
- [go-shellquote](https://github.com/kballard/go-shellquote)
|
||||
- [filetype](https://github.com/h2non/filetype)
|
||||
- [yaml](https://github.com/go-yaml/yaml/tree/v3)
|
||||
- [filetype](https://github.com/h2non/filetype) - MIME type detection
|
||||
- [gofast](https://github.com/yookoala/gofast) - FastCGI client
|
||||
- [go-shellquote](https://github.com/kballard/go-shellquote) - Shell string quoting
|
||||
- [yaml](https://github.com/go-yaml/yaml/tree/v3) - Configuration parsing
|
||||
|
|
36
config.go
36
config.go
|
@ -5,12 +5,15 @@ import (
|
|||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/yookoala/gofast"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
@ -32,6 +35,12 @@ type pathConfig struct {
|
|||
// List directory entries
|
||||
ListDirectory bool
|
||||
|
||||
// Content type
|
||||
Type string
|
||||
|
||||
// FastCGI server address
|
||||
FastCGI string
|
||||
|
||||
r *regexp.Regexp
|
||||
cmd []string
|
||||
}
|
||||
|
@ -49,8 +58,9 @@ type serverConfig struct {
|
|||
|
||||
Hosts map[string]*hostConfig
|
||||
|
||||
hostname string
|
||||
port int
|
||||
hostname string
|
||||
port int
|
||||
fcgiPools map[string]*gofast.ClientPool
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
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 != "")) ||
|
||||
(serve.Proxy != "" && (serve.Root != "" || serve.Command != "")) ||
|
||||
(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] == '^' {
|
||||
|
@ -118,7 +129,22 @@ func readconfig(configPath string) error {
|
|||
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)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse command %s: %s", serve.cmd, err)
|
||||
|
|
3
go.mod
3
go.mod
|
@ -5,6 +5,7 @@ go 1.15
|
|||
require (
|
||||
github.com/h2non/filetype v1.1.0
|
||||
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
|
||||
)
|
||||
|
|
42
go.sum
42
go.sum
|
@ -1,12 +1,46 @@
|
|||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM=
|
||||
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/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/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.9.0 h1:Iz4ywRDrfsyoR8xZOkSKGXXftMR2spIV6ibVuhrKvSw=
|
||||
github.com/makeworld-the-better-one/go-gemini v0.9.0/go.mod h1:P7/FbZ+IEIbA/d+A0Y3w2GNgD8SA2AcNv7aDGJbaWG4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
118
server.go
118
server.go
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -20,10 +21,34 @@ import (
|
|||
"unicode/utf8"
|
||||
|
||||
"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) {
|
||||
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) {
|
||||
var meta string
|
||||
switch code {
|
||||
case gemini.StatusTemporaryFailure:
|
||||
case statusTemporaryFailure:
|
||||
meta = "Temporary failure"
|
||||
case gemini.StatusProxyError:
|
||||
case statusProxyError:
|
||||
meta = "Proxy error"
|
||||
case gemini.StatusBadRequest:
|
||||
case statusBadRequest:
|
||||
meta = "Bad request"
|
||||
case gemini.StatusNotFound:
|
||||
case statusNotFound:
|
||||
meta = "Not found"
|
||||
case gemini.StatusProxyRequestRefused:
|
||||
case statusProxyRequestRefused:
|
||||
meta = "Proxy request refused"
|
||||
}
|
||||
writeHeader(c, code, meta)
|
||||
|
@ -74,7 +99,7 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusTemporaryFailure)
|
||||
writeStatus(c, statusTemporaryFailure)
|
||||
return
|
||||
}
|
||||
// List directories first
|
||||
|
@ -87,7 +112,7 @@ func serveDirectory(c net.Conn, request *url.URL, dirPath string) {
|
|||
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)
|
||||
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) {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusNotFound)
|
||||
writeStatus(c, statusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -152,7 +177,7 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
|
|||
if requestData[len(requestData)-1] != '/' {
|
||||
// Add trailing slash
|
||||
log.Println(requestData)
|
||||
writeHeader(c, gemini.StatusRedirectPermanent, requestData+"/")
|
||||
writeHeader(c, statusRedirectPermanent, requestData+"/")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -172,10 +197,10 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
|
|||
serveDirectory(c, request, originalPath)
|
||||
return
|
||||
}
|
||||
writeStatus(c, gemini.StatusNotFound)
|
||||
writeStatus(c, statusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
writeStatus(c, gemini.StatusTemporaryFailure)
|
||||
writeStatus(c, statusTemporaryFailure)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -202,7 +227,7 @@ func serveFile(c net.Conn, request *url.URL, requestData, filePath string, listD
|
|||
if mimeType == "" {
|
||||
mimeType = "text/gemini; charset=utf-8"
|
||||
}
|
||||
writeHeader(c, gemini.StatusSuccess, mimeType)
|
||||
writeHeader(c, statusSuccess, mimeType)
|
||||
|
||||
// Write body
|
||||
c.Write(buf[:n])
|
||||
|
@ -221,7 +246,7 @@ func serveProxy(c net.Conn, requestData, proxyURL string) {
|
|||
}
|
||||
proxy, err := tls.Dial("tcp", proxyURL, tlsConfig)
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusProxyError)
|
||||
writeStatus(c, statusProxyError)
|
||||
return
|
||||
}
|
||||
defer proxy.Close()
|
||||
|
@ -254,11 +279,11 @@ func serveCommand(c net.Conn, userInput string, command []string) {
|
|||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusProxyError)
|
||||
writeStatus(c, statusProxyError)
|
||||
return
|
||||
}
|
||||
|
||||
writeHeader(c, gemini.StatusSuccess, "text/gemini; charset=utf-8")
|
||||
writeHeader(c, statusSuccess, "text/gemini; charset=utf-8")
|
||||
c.Write(buf.Bytes())
|
||||
|
||||
if verbose {
|
||||
|
@ -293,7 +318,7 @@ func replaceWithUserInput(command []string, userInput string) []string {
|
|||
return newCommand
|
||||
}
|
||||
|
||||
func handleConn(c net.Conn) {
|
||||
func handleConn(c *tls.Conn) {
|
||||
if verbose {
|
||||
t := time.Now()
|
||||
defer func() {
|
||||
|
@ -318,28 +343,39 @@ func handleConn(c net.Conn) {
|
|||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Println(scanner.Text(), "FAILED")
|
||||
writeStatus(c, gemini.StatusBadRequest)
|
||||
writeStatus(c, statusBadRequest)
|
||||
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 {
|
||||
log.Printf("> %s\n", requestData)
|
||||
}
|
||||
|
||||
if len(requestData) > gemini.URLMaxLength || !utf8.ValidString(requestData) {
|
||||
writeStatus(c, gemini.StatusBadRequest)
|
||||
if len(requestData) > urlMaxLength || !utf8.ValidString(requestData) {
|
||||
writeStatus(c, statusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
request, err := url.Parse(requestData)
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusBadRequest)
|
||||
writeStatus(c, statusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
requestHostname := request.Hostname()
|
||||
if requestHostname == "" || strings.ContainsRune(requestHostname, ' ') {
|
||||
writeStatus(c, gemini.StatusBadRequest)
|
||||
writeStatus(c, statusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -355,12 +391,12 @@ func handleConn(c net.Conn) {
|
|||
request.Scheme = "gemini"
|
||||
}
|
||||
if request.Scheme != "gemini" || (requestPort > 0 && requestPort != config.port) {
|
||||
writeStatus(c, gemini.StatusProxyRequestRefused)
|
||||
writeStatus(c, statusProxyRequestRefused)
|
||||
}
|
||||
|
||||
if request.Path == "" {
|
||||
// Redirect to /
|
||||
writeHeader(c, gemini.StatusRedirectPermanent, requestData+"/")
|
||||
writeHeader(c, statusRedirectPermanent, requestData+"/")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -371,7 +407,7 @@ func handleConn(c net.Conn) {
|
|||
}
|
||||
requestQuery, err := url.QueryUnescape(request.RawQuery)
|
||||
if err != nil {
|
||||
writeStatus(c, gemini.StatusBadRequest)
|
||||
writeStatus(c, statusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -392,10 +428,10 @@ func handleConn(c net.Conn) {
|
|||
requireInput := serve.Input != "" || serve.SensitiveInput != ""
|
||||
if requestQuery == "" && requireInput {
|
||||
if serve.Input != "" {
|
||||
writeHeader(c, gemini.StatusInput, serve.Input)
|
||||
writeHeader(c, statusInput, serve.Input)
|
||||
return
|
||||
} else if serve.SensitiveInput != "" {
|
||||
writeHeader(c, gemini.StatusSensitiveInput, serve.SensitiveInput)
|
||||
writeHeader(c, statusSensitiveInput, serve.SensitiveInput)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -404,6 +440,16 @@ func handleConn(c net.Conn) {
|
|||
if serve.Proxy != "" {
|
||||
serveProxy(c, requestData, serve.Proxy)
|
||||
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 {
|
||||
if requireInput {
|
||||
newCommand := replaceWithUserInput(serve.cmd, requestQuery)
|
||||
|
@ -421,6 +467,16 @@ func handleConn(c net.Conn) {
|
|||
if serve.Proxy != "" {
|
||||
serveProxy(c, requestData, serve.Proxy)
|
||||
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 {
|
||||
if requireInput {
|
||||
newCommand := replaceWithUserInput(serve.cmd, requestQuery)
|
||||
|
@ -444,9 +500,9 @@ func handleConn(c net.Conn) {
|
|||
}
|
||||
|
||||
if matchedHost {
|
||||
writeStatus(c, gemini.StatusNotFound)
|
||||
writeStatus(c, statusNotFound)
|
||||
} else {
|
||||
writeStatus(c, gemini.StatusProxyRequestRefused)
|
||||
writeStatus(c, statusProxyRequestRefused)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -468,7 +524,7 @@ func handleListener(l net.Listener) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go handleConn(conn)
|
||||
go handleConn(conn.(*tls.Conn))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
57
server_fcgi.go
Normal file
57
server_fcgi.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue