diff --git a/fastcgi/client.go b/fastcgi/client.go index 1108d85..5b4aaf9 100644 --- a/fastcgi/client.go +++ b/fastcgi/client.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "fmt" "io" + "log" "net/http" "net/http/httputil" "net/textproto" @@ -28,108 +29,122 @@ func (client *FCGIClient) Close() { client.rwc.Close() } -func (client *FCGIClient) writeRecord(recType FCGIRequestType, content []byte) (err error) { +func (client *FCGIClient) writeRecord(r *Record) (err error) { client.mutex.Lock() defer client.mutex.Unlock() client.buf.Reset() // Write the record to the connection - rec := NewRecord(recType, content) - b, err := rec.toBytes() + b, err := r.toBytes() _, err = client.rwc.Write(b) return err } -func (client *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error { - b := make([]byte, 8) - binary.BigEndian.PutUint32(b, uint32(appStatus)) - b[4] = protocolStatus - return client.writeRecord(FCGI_END_REQUEST, b) -} - -// Spec: https://www.mit.edu/~yandros/doc/specs/fcgi-spec.html#S3 -// Name value pairs such as: SCRIPT_PATH = /some/path -// Should be encoded as such: -// Name size -// Value size -// Name -// Value -type NameValuePair struct { - // Making the length values 32 bit for ease. - // However, when encoding, the rules for - // how many bytes are used will apply. - NameLength uint32 - ValueLength uint32 - // Data - NameData string - ValueData string -} - -func (client *FCGIClient) writePairs(recType FCGIRequestType, pairs map[string]string) error { - // Get ourselves a nice slice to work with - nvpairs := []NameValuePair{} - for k, v := range pairs { - nvpairs = append(nvpairs, NameValuePair{ - NameLength: uint32(len(k)), - ValueLength: uint32(len(v)), - NameData: k, - ValueData: v, - }) +// We write long data such as FCGI_PARAMS or FCGI_STDIN +// as a stream. If a content's body is larger than +// maxWrite, we will split it into separate records. +// maxWrite is determined by the size of the +// contentLength field in the header (2 bytes) +// Meaning that the max value for contentLength +// we can encode is 65535. +func (c *FCGIClient) writeStream(records []Record) error { + if len(records) == 0 { + return nil } - // We'll use this to put together - // the packet - var buf bytes.Buffer - - for _, p := range nvpairs { - // Let's see how many bytes we have in total. - // Since we have to leave 8 bytes for encoding - // the sizes, we'll add it to the calculation. - // If the value is larger than what we can - // handle, we'll truncate it - if (8 + p.NameLength + p.ValueLength) > maxWrite { - fmt.Println("We should not have hit this") - p.ValueLength = maxWrite - 8 - p.NameLength - p.ValueData = p.ValueData[:p.ValueLength] - } - - // The high bit of name size and value size is used for signaling - // how many bytes are used to store the length/size. - // If the size is > 127, we can just use one byte, - // and the high bit will be 0, otherwise, we use - // four bytes and the high bit will be 1 - // So if length is encoded in 4 bytes it would look - // something like: - // 10000000000000000000010000100000 - if p.NameLength > 127 { - p.NameLength |= 1 << 31 - b := make([]byte, 4) - binary.BigEndian.PutUint32(b, p.NameLength) - buf.Write(b) - } else { - buf.Write([]byte{byte(p.NameLength)}) - } - - if p.ValueLength > 127 { - p.ValueLength |= 1 << 31 - b := make([]byte, 4) - binary.BigEndian.PutUint32(b, p.ValueLength) - buf.Write(b) - } else { - buf.Write([]byte{byte(p.ValueLength)}) - } - - // Now we just write our values to the buffer - buf.WriteString(p.NameData) - buf.WriteString(p.ValueData) + log.Printf("Writing %d records of type %d to stream", len(records), records[0].Header.Type) + for _, r := range records { + // Write the piece of the record to the stream + c.writeRecord(&r) } - w := newWriter(client, recType) - defer w.Close() + // 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 := NewRecord(records[0].Header.Type, nil) + c.writeRecord(end) + return nil +} - // Send the data - w.Write(buf.Bytes()) - w.Flush() +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) + + log.Println("Reading record contents") + 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()) + } + + log.Println("Read a record") + + 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() error { + err := c.writeRecord(NewBeginRequestRecord()) + if err != nil { + return err + } return nil } @@ -137,16 +152,12 @@ func (client *FCGIClient) writePairs(recType FCGIRequestType, pairs map[string]s // 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) { - beginRequestRecord := NewBeginRequestRecord() - err := client.writeRecord(beginRequestRecord.Header.Type, beginRequestRecord.Content) - if err != nil { - return http.Response{}, err - } + client.BeginRequest() - err = client.writePairs(FCGI_PARAMS, req.Context) - if err != nil { - return http.Response{}, err - } + // Write the request context as a stream + log.Println("Sending FCGI_PARAMS") + client.writeStream(req.EncodeContext()) + log.Println("Done") // body := newWriter(client, FCGI_STDIN) // if req != nil { @@ -154,9 +165,18 @@ func (client *FCGIClient) Do(req *FCGIRequest) (http.Response, error) { // } // body.Close() - r := &streamReader{c: client} - rb := bufio.NewReader(r) - tp := textproto.NewReader(rb) + // Read the app response from the FCGI_STDOUT stream + respContent := client.readResponse() + + fmt.Println(parseHttp(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) // Parse the first line of the response. line, err := tp.ReadLine() @@ -201,10 +221,12 @@ func (client *FCGIClient) Do(req *FCGIRequest) (http.Response, error) { resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) if chunked(resp.TransferEncoding) { - resp.Body = io.NopCloser(httputil.NewChunkedReader(rb)) + resp.Body = io.NopCloser(httputil.NewChunkedReader(bf)) } else { - resp.Body = io.NopCloser(rb) + resp.Body = io.NopCloser(bf) } + log.Println("Done") + return *resp, nil } diff --git a/fastcgi/fastcgi.go b/fastcgi/fastcgi.go index 841029b..8f1cedb 100644 --- a/fastcgi/fastcgi.go +++ b/fastcgi/fastcgi.go @@ -1,61 +1,10 @@ package fastcgi import ( - "bufio" - "encoding/binary" "fmt" - "io" "net" ) -type FCGIRequestType uint8 - -const FCGI_LISTENSOCK_FILENO uint8 = 0 -const FCGI_HEADER_LEN uint8 = 8 -const VERSION_1 uint8 = 1 -const FCGI_NULL_REQUEST_ID uint8 = 0 -const FCGI_KEEP_CONN uint8 = 1 -const doubleCRLF = "\r\n\r\n" - -const ( - FCGI_BEGIN_REQUEST FCGIRequestType = iota + 1 - FCGI_ABORT_REQUEST - FCGI_END_REQUEST - FCGI_PARAMS - FCGI_STDIN - FCGI_STDOUT - FCGI_STDERR - FCGI_DATA - FCGI_GET_VALUES - FCGI_GET_VALUES_RESULT - FCGI_UNKNOWN_TYPE - FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE -) - -const ( - FCGI_RESPONDER uint8 = iota + 1 - FCGI_AUTHORIZER - FCGI_FILTER -) - -const ( - FCGI_REQUEST_COMPLETE uint8 = iota - FCGI_CANT_MPX_CONN - FCGI_OVERLOADED - FCGI_UNKNOWN_ROLE -) - -const ( - FCGI_MAX_CONNS string = "MAX_CONNS" - FCGI_MAX_REQS string = "MAX_REQS" - FCGI_MPXS_CONNS string = "MPXS_CONNS" -) - -const ( - maxWrite = 65500 // 65530 may work, but for compatibility - maxPad = 255 -) - // Connects to the fcgi responder at the specified network address. // See func net.Dial for a description of the network and address parameters. func Dial(network, address string) (fcgi *FCGIClient, err error) { @@ -75,50 +24,6 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) { return } -func readSize(s []byte) (uint32, int) { - if len(s) == 0 { - return 0, 0 - } - size, n := uint32(s[0]), 1 - if size&(1<<7) != 0 { - if len(s) < 4 { - return 0, 0 - } - n = 4 - size = binary.BigEndian.Uint32(s) - size &^= 1 << 31 - } - return size, n -} - -func readString(s []byte, size uint32) string { - if size > uint32(len(s)) { - return "" - } - return string(s[:size]) -} - -// bufWriter encapsulates bufio.Writer but also closes the underlying stream when -// Closed. -type bufWriter struct { - closer io.Closer - *bufio.Writer -} - -func (w *bufWriter) Close() error { - if err := w.Writer.Flush(); err != nil { - w.closer.Close() - return err - } - return w.closer.Close() -} - -func newWriter(c *FCGIClient, recType FCGIRequestType) *bufWriter { - s := &streamWriter{c: c, recType: recType} - w := bufio.NewWriterSize(s, maxWrite) - return &bufWriter{s, w} -} - type badStringError struct { what string str string diff --git a/fastcgi/request.go b/fastcgi/request.go index ba39471..c4e35bb 100644 --- a/fastcgi/request.go +++ b/fastcgi/request.go @@ -3,126 +3,17 @@ package fastcgi import ( "bytes" "encoding/binary" - "errors" "io" + "log" "net/http" "net/url" "strconv" ) -// -- TYPES -- -// These are the types needed to house the data for an FCGI request -type Header struct { - Version uint8 - Type FCGIRequestType - Id uint16 - ContentLength uint16 - PaddingLength uint8 - Reserved uint8 -} - -func (h *Header) init(reqType FCGIRequestType, reqId uint16, l int) { -} - -// A record is essentially a "packet" in FastCGI. -// The header lets the server know what type -// of data is being sent, and it expects -// a certain structure depending on -// the type. For example, the -// FCGI_BEGIN_REQUEST record should -// have a body of 8 bytes with: -// - The first byte being the role -// - The second byte being also the role -// - The third byte being the flags -// - The last five bytes are reserved for future use -type Record struct { - Header Header - Content []byte - Buffer bytes.Buffer // Buffer to use when writing to the network - ReadBuffer []byte // Buffer to use when reading a response -} - -func NewRecord(t FCGIRequestType, content []byte) *Record { - r := Record{} - - r.Header.Version = 1 - r.Header.Type = t - r.Header.Id = 1 - r.Header.ContentLength = uint16(len(content)) - r.Header.PaddingLength = uint8(-len(content) & 7) - r.Content = content - - return &r -} - -func NewBeginRequestRecord() *Record { - role := uint16(FCGI_RESPONDER) - flags := byte(0) - // Create an 8-byte array as per the FastCGI specification. - var b [8]byte - - // Split the 16-bit role into two bytes and assign them. - b[0] = byte(role >> 8) // High byte - b[1] = byte(role) // Low byte - - // Set the flags. - b[2] = flags - - // The reserved bytes (b[3] to b[7]) will remain zero by default. - - // Return a begin request record - return NewRecord(FCGI_BEGIN_REQUEST, b[:]) -} - -// Turn a record into a byte array so it can be -// sent over the network socket -func (r *Record) toBytes() ([]byte, error) { - // client.h.init(recType, client.reqId, len(content)) - if err := binary.Write(&r.Buffer, binary.BigEndian, r.Header); err != nil { - return nil, err - } - if _, err := r.Buffer.Write(r.Content); err != nil { - return nil, err - } - if _, err := r.Buffer.Write(pad[:r.Header.PaddingLength]); err != nil { - return nil, err - } - - return r.Buffer.Bytes(), nil -} - -func (rec *Record) read(r io.Reader) (buf []byte, err error) { - if err = binary.Read(r, binary.BigEndian, &rec.Header); err != nil { - return - } - if rec.Header.Version != 1 { - err = errors.New("fcgi: invalid header version") - return - } - if rec.Header.Type == FCGI_END_REQUEST { - err = io.EOF - return - } - n := int(rec.Header.ContentLength) + int(rec.Header.PaddingLength) - if len(rec.ReadBuffer) < n { - rec.ReadBuffer = make([]byte, n) - } - if n, err = io.ReadFull(r, rec.ReadBuffer[:n]); err != nil { - return - } - buf = rec.ReadBuffer[:int(rec.Header.ContentLength)] - - return -} - -// for padding so we don't have to allocate all the time -// not synchronized because we don't care what the contents are -var pad [maxPad]byte - type FCGIRequest struct { Id uint16 - Type FCGIRequestType Context map[string]string + Body []byte Records []Record } @@ -212,36 +103,72 @@ func (r *FCGIRequest) TypePostForm(data url.Values) { // // return client.Post(p, bodyType, buf, buf.Len()) // } -// Format the context for sending -// func (r *FCGIRequest) writePairs() error { -// w := newWriter(client, recType) -// b := make([]byte, 8) -// nn := 0 -// for k, v := range r.Context { -// m := 8 + len(k) + len(v) -// if m > maxWrite { -// // param data size exceed 65535 bytes" -// vl := maxWrite - 8 - len(k) -// v = v[:vl] -// } -// n := encodeSize(b, uint32(len(k))) -// n += encodeSize(b[n:], uint32(len(v))) -// m = n + len(k) + len(v) -// if (nn + m) > maxWrite { -// w.Flush() -// nn = 0 -// } -// nn += m -// if _, err := w.Write(b[:n]); err != nil { -// return err -// } -// if _, err := w.WriteString(k); err != nil { -// return err -// } -// if _, err := w.WriteString(v); err != nil { -// return err -// } -// } -// w.Close() -// return nil -// } +// Spec: https://www.mit.edu/~yandros/doc/specs/fcgi-spec.html#S3 +// Name value pairs such as: SCRIPT_PATH = /some/path +// Should be encoded as such: +// Name size +// Value size +// Name +// Value +// We'll encode the context correctly and return +// a slice of records to send. +func (req *FCGIRequest) EncodeContext() []Record { + records := []Record{} + for k, v := range req.Context { + // We'll use this to put together + // the body of the record + var buf bytes.Buffer + + // Let's see how many bytes we have in total. + // Since we have to leave 8 bytes for encoding + // the sizes, we'll add it to the calculation. + // If the value is larger than what we can + // handle, we'll truncate it. + // log.Printf("Encoding %s(%d) = %s(%d)\n", k, len(k), v, len(v)) + if (8 + len(k) + len(v)) > maxWrite { + valMaxLength := maxWrite - 8 - len(k) + v = v[:valMaxLength] + } + + // The high bit of name size and value size is used for signaling + // how many bytes are used to store the length/size. + // If the size is > 127, we can just use one byte, + // and the high bit will be 0, otherwise, we use + // four bytes and the high bit will be 1 + // So if length is encoded in 4 bytes it would look + // something like: + // 10000000000000000000010000100000 + // For lengths < 127, we just use + // one byte with a high bit of 0 + // 01001001 + if len(k) > 127 { + size := uint32(len(k)) + size |= 1 << 31 // Set the high bit to 1 + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, size) + buf.Write(b) + } else { + buf.Write([]byte{byte(len(k))}) + } + + if len(v) > 127 { + size := uint32(len(v)) + size |= 1 << 31 + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, size) + buf.Write(b) + } else { + buf.Write([]byte{byte(len(v))}) + } + + // Now we just write our values to the buffer + buf.WriteString(k) + buf.WriteString(v) + + records = append(records, *NewRecord(FCGI_PARAMS, buf.Bytes())) + buf.Reset() + } + + log.Printf("We are sending %d FCGI_PARAMS records", len(records)) + return records +} diff --git a/fastcgi/streamreader.go b/fastcgi/streamreader.go deleted file mode 100644 index 4d685a6..0000000 --- a/fastcgi/streamreader.go +++ /dev/null @@ -1,28 +0,0 @@ -package fastcgi - -type streamReader struct { - c *FCGIClient - buf []byte -} - -func (w *streamReader) Read(p []byte) (n int, err error) { - - if len(p) > 0 { - if len(w.buf) == 0 { - rec := &Record{} - w.buf, err = rec.read(w.c.rwc) - if err != nil { - return - } - } - - n = len(p) - if n > len(w.buf) { - n = len(w.buf) - } - copy(p, w.buf[:n]) - w.buf = w.buf[n:] - } - - return -} diff --git a/fastcgi/streamwriter.go b/fastcgi/streamwriter.go deleted file mode 100644 index 26e97fb..0000000 --- a/fastcgi/streamwriter.go +++ /dev/null @@ -1,29 +0,0 @@ -package fastcgi - -// streamWriter abstracts out the separation of a stream into discrete records. -// It only writes maxWrite bytes at a time. -type streamWriter struct { - c *FCGIClient - recType FCGIRequestType -} - -func (w *streamWriter) Write(p []byte) (int, error) { - nn := 0 - for len(p) > 0 { - n := len(p) - if n > maxWrite { - n = maxWrite - } - if err := w.c.writeRecord(w.recType, p[:n]); err != nil { - return nn, err - } - nn += n - p = p[n:] - } - return nn, nil -} - -func (w *streamWriter) Close() error { - // send empty record to close the stream - return w.c.writeRecord(w.recType, nil) -} diff --git a/main.go b/main.go index 2f5ca58..fd616c1 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,8 @@ func handleRequest(w http.ResponseWriter, r *http.Request) { req.TypeGet() fcgiClient, err := fastcgi.Dial("unix", "/var/run/php/php8.3-fpm.sock") + defer fcgiClient.Close() + if err != nil { log.Println("err:", err) }