This commit is contained in:
Javier Feliz 2024-12-28 12:35:22 -05:00
parent 10d8e5bf42
commit 677083b071
6 changed files with 197 additions and 398 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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