view 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 source

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