diff fcgi/fcgi.go @ 0:48bdab3eec8a

Initial
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 00:37:49 +0500
parents
children eb705d4cdcd7
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fcgi/fcgi.go	Mon Mar 09 00:37:49 2026 +0500
@@ -0,0 +1,195 @@
+// Package fcgi implements a minimal FastCGI client.
+//
+// Supports sending a single request over a pre-dialed net.Conn and streaming
+// the CGI response back to an http.ResponseWriter.
+package fcgi
+
+import (
+	"bufio"
+	"encoding/binary"
+	"io"
+	"net/http"
+	"net/textproto"
+	"strings"
+)
+
+// Reference: https://fastcgi-archives.github.io/FastCGI_Specification.html
+
+const (
+	fcgiVersion       = 1
+	fcgiBeginRequest  = 1
+	fcgiParams        = 4
+	fcgiStdin         = 5
+	fcgiStdout        = 6
+	fcgiEndRequest    = 3
+	fcgiRoleResponder = 1
+	fcgiRequestID     = 1
+)
+
+// Do sends params and the request body over conn as a FastCGI request, then
+// parses the response and writes it to w. conn is closed by the caller.
+func Do(w http.ResponseWriter, r *http.Request, conn io.ReadWriter, params map[string]string) error {
+	bw := bufio.NewWriter(conn)
+
+	if err := writeRecord(bw, fcgiBeginRequest, fcgiRequestID,
+		[]byte{0, fcgiRoleResponder, 0, 0, 0, 0, 0, 0}); err != nil {
+		return err
+	}
+
+	if err := writeRecord(bw, fcgiParams, fcgiRequestID, encodeParams(params)); err != nil {
+		return err
+	}
+	if err := writeRecord(bw, fcgiParams, fcgiRequestID, nil); err != nil {
+		return err
+	}
+
+	if r.Body != nil {
+		buf := make([]byte, 4096)
+		for {
+			n, err := r.Body.Read(buf)
+			if n > 0 {
+				if werr := writeRecord(bw, fcgiStdin, fcgiRequestID, buf[:n]); werr != nil {
+					return werr
+				}
+			}
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				return err
+			}
+		}
+	}
+	if err := writeRecord(bw, fcgiStdin, fcgiRequestID, nil); err != nil {
+		return err
+	}
+	if err := bw.Flush(); err != nil {
+		return err
+	}
+
+	// Read response
+	var stdout strings.Builder
+	br := bufio.NewReader(conn)
+
+	for {
+		hdr := make([]byte, 8)
+		if _, err := io.ReadFull(br, hdr); err != nil {
+			return err
+		}
+		recType := hdr[1]
+		contentLen := int(binary.BigEndian.Uint16(hdr[4:6]))
+		paddingLen := int(hdr[6])
+
+		content := make([]byte, contentLen)
+		if _, err := io.ReadFull(br, content); err != nil {
+			return err
+		}
+		if paddingLen > 0 {
+			if _, err := io.ReadFull(br, make([]byte, paddingLen)); err != nil {
+				return err
+			}
+		}
+
+		switch recType {
+		case fcgiStdout:
+			stdout.Write(content)
+		case fcgiEndRequest:
+			return forwardResponse(w, stdout.String())
+		}
+		// fcgiStderr is silently discarded
+	}
+}
+
+func forwardResponse(w http.ResponseWriter, raw string) error {
+	sep := "\r\n\r\n"
+	idx := strings.Index(raw, sep)
+	advance := 4
+	if idx == -1 {
+		sep = "\n\n"
+		idx = strings.Index(raw, sep)
+		advance = 2
+	}
+	if idx == -1 {
+		w.Write([]byte(raw))
+		return nil
+	}
+
+	headerPart := raw[:idx]
+	body := raw[idx+advance:]
+
+	tp := textproto.NewReader(bufio.NewReader(strings.NewReader(headerPart + "\r\n\r\n")))
+	mime, _ := tp.ReadMIMEHeader()
+
+	status := 200
+	if s := mime.Get("Status"); s != "" {
+		code, _, _ := strings.Cut(s, " ")
+		if n := parseIntFast(code); n > 0 {
+			status = n
+		}
+		mime.Del("Status")
+	}
+
+	for k, vs := range mime {
+		for _, v := range vs {
+			w.Header().Add(k, v)
+		}
+	}
+	w.WriteHeader(status)
+	w.Write([]byte(body))
+	return nil
+}
+
+func writeRecord(w io.Writer, recType uint8, reqID uint16, content []byte) error {
+	length := len(content)
+	padding := (8 - (length % 8)) % 8
+	hdr := []byte{
+		fcgiVersion, recType,
+		byte(reqID >> 8), byte(reqID),
+		byte(length >> 8), byte(length),
+		byte(padding), 0,
+	}
+	if _, err := w.Write(hdr); err != nil {
+		return err
+	}
+	if len(content) > 0 {
+		if _, err := w.Write(content); err != nil {
+			return err
+		}
+	}
+	if padding > 0 {
+		if _, err := w.Write(make([]byte, padding)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func encodeParams(params map[string]string) []byte {
+	var buf []byte
+	for k, v := range params {
+		buf = appendLen(buf, len(k))
+		buf = appendLen(buf, len(v))
+		buf = append(buf, k...)
+		buf = append(buf, v...)
+	}
+	return buf
+}
+
+func appendLen(buf []byte, n int) []byte {
+	if n <= 127 {
+		return append(buf, byte(n))
+	}
+	return append(buf, byte((n>>24)|0x80), byte(n>>16), byte(n>>8), byte(n))
+}
+
+func parseIntFast(s string) int {
+	n := 0
+	for i := 0; i < len(s); i++ {
+		c := s[i]
+		if c < '0' || c > '9' {
+			return 0
+		}
+		n = n*10 + int(c-'0')
+	}
+	return n
+}