mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
147 lines
3.7 KiB
Go
147 lines
3.7 KiB
Go
package protocol
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"strings"
|
|
)
|
|
|
|
// HTTPProtocol converts stdin/stdout streams into HTTP/1.1 compliant
|
|
// communication. It relies on Content-Length to know when to stop reading from
|
|
// containers stdout. It also mandates valid HTTP headers back and forth, thus
|
|
// returning errors in case of parsing problems.
|
|
type HTTPProtocol struct {
|
|
in io.Writer
|
|
out io.Reader
|
|
}
|
|
|
|
func (p *HTTPProtocol) IsStreamable() bool { return true }
|
|
|
|
// this is just an http.Handler really
|
|
// TODO handle req.Context better with io.Copy. io.Copy could push us
|
|
// over the timeout.
|
|
// TODO maybe we should take io.Writer, io.Reader but then we have to
|
|
// dump the request to a buffer again :(
|
|
func (h *HTTPProtocol) Dispatch(w io.Writer, req *http.Request) error {
|
|
err := DumpRequestTo(h.in, req) // TODO timeout
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rw, ok := w.(http.ResponseWriter); ok {
|
|
// if we're writing directly to the response writer, we need to set headers
|
|
// and status code first since calling res.Write will just write the http
|
|
// response as the body (headers and all)
|
|
|
|
res, err := http.ReadResponse(bufio.NewReader(h.out), req) // TODO timeout
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k, vs := range res.Header {
|
|
for _, v := range vs {
|
|
rw.Header().Add(k, v) // on top of any specified on the route
|
|
}
|
|
}
|
|
rw.WriteHeader(res.StatusCode)
|
|
// TODO should we TCP_CORK ?
|
|
|
|
io.Copy(rw, res.Body) // TODO timeout
|
|
res.Body.Close()
|
|
} else {
|
|
// logs can just copy the full thing in there, headers and all.
|
|
|
|
res, err := http.ReadResponse(bufio.NewReader(h.out), req) // TODO timeout
|
|
if err != nil {
|
|
return err
|
|
}
|
|
res.Write(w) // TODO timeout
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DumpRequestTo is httputil.DumpRequest with some modifications. It will
|
|
// dump the request to the provided io.Writer with the body always, consuming
|
|
// the body in the process.
|
|
//
|
|
// TODO we should support h2!
|
|
func DumpRequestTo(w io.Writer, req *http.Request) error {
|
|
// By default, print out the unmodified req.RequestURI, which
|
|
// is always set for incoming server requests. But because we
|
|
// previously used req.URL.RequestURI and the docs weren't
|
|
// always so clear about when to use DumpRequest vs
|
|
// DumpRequestOut, fall back to the old way if the caller
|
|
// provides a non-server Request.
|
|
|
|
reqURI := req.RequestURI
|
|
if reqURI == "" {
|
|
reqURI = req.URL.RequestURI()
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s %s HTTP/%d.%d\r\n", valueOrDefault(req.Method, "GET"),
|
|
reqURI, req.ProtoMajor, req.ProtoMinor)
|
|
|
|
absRequestURI := strings.HasPrefix(req.RequestURI, "http://") || strings.HasPrefix(req.RequestURI, "https://")
|
|
if !absRequestURI {
|
|
host := req.Host
|
|
if host == "" && req.URL != nil {
|
|
host = req.URL.Host
|
|
}
|
|
|
|
if host != "" {
|
|
fmt.Fprintf(w, "Host: %s\r\n", host)
|
|
}
|
|
}
|
|
|
|
chunked := len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked"
|
|
|
|
if len(req.TransferEncoding) > 0 {
|
|
fmt.Fprintf(w, "Transfer-Encoding: %s\r\n", strings.Join(req.TransferEncoding, ","))
|
|
}
|
|
|
|
if req.Close {
|
|
fmt.Fprintf(w, "Connection: close\r\n")
|
|
}
|
|
|
|
err := req.Header.WriteSubset(w, reqWriteExcludeHeaderDump)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
io.WriteString(w, "\r\n")
|
|
|
|
if req.Body != nil {
|
|
var dest io.Writer = w
|
|
if chunked {
|
|
dest = httputil.NewChunkedWriter(dest)
|
|
}
|
|
|
|
// TODO copy w/ ctx
|
|
_, err = io.Copy(dest, req.Body)
|
|
if chunked {
|
|
dest.(io.Closer).Close()
|
|
io.WriteString(w, "\r\n")
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
var reqWriteExcludeHeaderDump = map[string]bool{
|
|
"Host": true, // not in Header map anyway
|
|
"Transfer-Encoding": true,
|
|
"Trailer": true,
|
|
}
|
|
|
|
// Return value if nonempty, def otherwise.
|
|
func valueOrDefault(value, def string) string {
|
|
if value != "" {
|
|
return value
|
|
}
|
|
return def
|
|
}
|