oasis/fastcgi/client.go
2024-12-29 17:58:01 -05:00

239 lines
5.8 KiB
Go

package fastcgi
import (
"bufio"
"bytes"
"encoding/binary"
"io"
"log"
"net/http"
"net/http/httputil"
"net/textproto"
"os"
"strconv"
"strings"
"sync"
)
type FCGIClient struct {
mutex sync.Mutex
rwc io.ReadWriteCloser
h Header
buf bytes.Buffer
keepAlive bool
reqId uint16
}
// Close fcgi connnection
func (client *FCGIClient) Close() {
client.rwc.Close()
}
func (client *FCGIClient) writeRecord(r *Record) (err error) {
client.mutex.Lock()
defer client.mutex.Unlock()
client.buf.Reset()
// Write the record to the connection
b, err := r.toBytes()
_, err = client.rwc.Write(b)
return err
}
// We write long data such as FCGI_PARAMS or FCGI_STDIN
// as a stream. Sending an empty record of the same
// type to signal the end.
func (c *FCGIClient) writeStream(req *FCGIRequest, records []Record) error {
if len(records) == 0 {
return nil
}
log.Printf("Writing %d records of type %d to stream", len(records), records[0].Header.Type)
for _, r := range records {
c.writeRecord(&r)
}
// Send an empty record to end the stream.
// all the records should be of the same
// type so we'll just use the type from
// the first item in the slice.
log.Println("Ending stream")
end := req.NewRecord(records[0].Header.Type, nil)
c.writeRecord(end)
return nil
}
func readRecord(r io.Reader) (*Record, error) {
// It's easier to read the header piece of the record
// into the struct as opposed to doing it piece by
// piece. But we'll do it this way to be explicit.
var version uint8
var recType FCGIRecordType
var id uint16
var contentlength uint16
var paddinglength uint8
var reserved uint8
// log.Println("Reading header of response record")
// Let's read the header fields of the record.
binary.Read(r, binary.BigEndian, &version)
binary.Read(r, binary.BigEndian, &recType)
binary.Read(r, binary.BigEndian, &id)
binary.Read(r, binary.BigEndian, &contentlength)
binary.Read(r, binary.BigEndian, &paddinglength)
binary.Read(r, binary.BigEndian, &reserved)
// log.Printf("Ver: %d Type: %d ID: %d Length: %d Padding: %d Reserved; %d", version, recType, id, contentlength, paddinglength, reserved)
readLength := int(contentlength) + int(paddinglength)
content := make([]byte, readLength)
if _, err := io.ReadFull(r, content); err != nil {
return nil, err
}
// Remove any padding from the content
content = content[:contentlength]
rec := Record{}
rec.Header.Version = version
rec.Header.Type = recType
rec.Header.Id = id
rec.Header.ContentLength = contentlength
rec.Header.PaddingLength = paddinglength
rec.Header.Reserved = reserved
rec.Content = content
return &rec, nil
}
func (c *FCGIClient) readResponse() []byte {
var response bytes.Buffer
log.Println("-- STARTING STDOUT READ --")
for {
r, err := readRecord(c.rwc)
if err != nil {
log.Printf("Encountered error when reading the stream: %s", err.Error())
}
if r.Header.Type == FCGI_END_REQUEST {
break
}
response.Write(r.Content)
}
log.Println("-- END STDOUT READ --")
return response.Bytes()
}
// func (c *FCGIClient) Get(req *http.Request, fcgiParams map[string]string) *http.Response {
//
// }
func (c *FCGIClient) BeginRequest(req *FCGIRequest) error {
err := c.writeRecord(req.NewBeginRequestRecord())
if err != nil {
return err
}
return nil
}
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (client *FCGIClient) Do(req *FCGIRequest) (http.Response, error) {
log.Printf("______REQUEST %d______", req.Id)
log.Println("-- REQUEST CONTEXT --")
for k, v := range req.Context {
log.Printf("[%s] = %s", k, v)
}
client.BeginRequest(req)
// Write the request context as a stream
log.Println("Sending FCGI_PARAMS")
client.writeStream(req, req.EncodeContext())
log.Println("Done")
// Write the body (if any)
body := req.EncodeBody()
if len(body) > 0 {
client.writeStream(req, body)
}
// Read the app response from the FCGI_STDOUT stream
respContent := client.readResponse()
log.Printf("______END REQUEST %d______", req.Id)
f, _ := os.Create("./resp.txt")
defer f.Close()
f.Write(respContent)
return parseHttp(respContent)
}
func parseHttp(raw []byte) (http.Response, error) {
log.Println("Parsing http")
bf := bufio.NewReader(bytes.NewReader(raw))
tp := textproto.NewReader(bf)
resp := new(http.Response)
// Ensure we have a valid http response
line, err := tp.ReadLine()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return http.Response{}, err
}
i := strings.IndexByte(line, ' ')
if i == -1 {
return http.Response{}, &badStringError{"malformed HTTP response", line}
}
resp.Proto = line[:i]
resp.Status = strings.TrimLeft(line[i+1:], " ")
statusCode := resp.Status
if i := strings.IndexByte(resp.Status, ' '); i != -1 {
statusCode = resp.Status[:i]
}
if len(statusCode) != 3 {
err = &badStringError{"malformed HTTP status code", statusCode}
}
resp.StatusCode, err = strconv.Atoi(statusCode)
if err != nil || resp.StatusCode < 0 {
err = &badStringError{"malformed HTTP status code", statusCode}
}
var ok bool
if resp.ProtoMajor, resp.ProtoMinor, ok = http.ParseHTTPVersion(resp.Proto); !ok {
err = &badStringError{"malformed HTTP version", resp.Proto}
}
// Parse the response headers.
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return http.Response{}, err
}
resp.Header = http.Header(mimeHeader)
// TODO: fixTransferEncoding ?
resp.TransferEncoding = resp.Header["Transfer-Encoding"]
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if chunked(resp.TransferEncoding) {
resp.Body = io.NopCloser(httputil.NewChunkedReader(bf))
} else {
resp.Body = io.NopCloser(bf)
}
log.Println("Done")
return *resp, nil
}