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
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)$

View File

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

View File

@ -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
View File

@ -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
View File

@ -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
View File

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