comparison fcgi/fcgi.go @ 0:48bdab3eec8a

Initial
author Atarwn Gard <a@qwa.su>
date Mon, 09 Mar 2026 00:37:49 +0500
parents
children eb705d4cdcd7
comparison
equal deleted inserted replaced
-1:000000000000 0:48bdab3eec8a
1 // Package fcgi implements a minimal FastCGI client.
2 //
3 // Supports sending a single request over a pre-dialed net.Conn and streaming
4 // the CGI response back to an http.ResponseWriter.
5 package fcgi
6
7 import (
8 "bufio"
9 "encoding/binary"
10 "io"
11 "net/http"
12 "net/textproto"
13 "strings"
14 )
15
16 // Reference: https://fastcgi-archives.github.io/FastCGI_Specification.html
17
18 const (
19 fcgiVersion = 1
20 fcgiBeginRequest = 1
21 fcgiParams = 4
22 fcgiStdin = 5
23 fcgiStdout = 6
24 fcgiEndRequest = 3
25 fcgiRoleResponder = 1
26 fcgiRequestID = 1
27 )
28
29 // Do sends params and the request body over conn as a FastCGI request, then
30 // parses the response and writes it to w. conn is closed by the caller.
31 func Do(w http.ResponseWriter, r *http.Request, conn io.ReadWriter, params map[string]string) error {
32 bw := bufio.NewWriter(conn)
33
34 if err := writeRecord(bw, fcgiBeginRequest, fcgiRequestID,
35 []byte{0, fcgiRoleResponder, 0, 0, 0, 0, 0, 0}); err != nil {
36 return err
37 }
38
39 if err := writeRecord(bw, fcgiParams, fcgiRequestID, encodeParams(params)); err != nil {
40 return err
41 }
42 if err := writeRecord(bw, fcgiParams, fcgiRequestID, nil); err != nil {
43 return err
44 }
45
46 if r.Body != nil {
47 buf := make([]byte, 4096)
48 for {
49 n, err := r.Body.Read(buf)
50 if n > 0 {
51 if werr := writeRecord(bw, fcgiStdin, fcgiRequestID, buf[:n]); werr != nil {
52 return werr
53 }
54 }
55 if err == io.EOF {
56 break
57 }
58 if err != nil {
59 return err
60 }
61 }
62 }
63 if err := writeRecord(bw, fcgiStdin, fcgiRequestID, nil); err != nil {
64 return err
65 }
66 if err := bw.Flush(); err != nil {
67 return err
68 }
69
70 // Read response
71 var stdout strings.Builder
72 br := bufio.NewReader(conn)
73
74 for {
75 hdr := make([]byte, 8)
76 if _, err := io.ReadFull(br, hdr); err != nil {
77 return err
78 }
79 recType := hdr[1]
80 contentLen := int(binary.BigEndian.Uint16(hdr[4:6]))
81 paddingLen := int(hdr[6])
82
83 content := make([]byte, contentLen)
84 if _, err := io.ReadFull(br, content); err != nil {
85 return err
86 }
87 if paddingLen > 0 {
88 if _, err := io.ReadFull(br, make([]byte, paddingLen)); err != nil {
89 return err
90 }
91 }
92
93 switch recType {
94 case fcgiStdout:
95 stdout.Write(content)
96 case fcgiEndRequest:
97 return forwardResponse(w, stdout.String())
98 }
99 // fcgiStderr is silently discarded
100 }
101 }
102
103 func forwardResponse(w http.ResponseWriter, raw string) error {
104 sep := "\r\n\r\n"
105 idx := strings.Index(raw, sep)
106 advance := 4
107 if idx == -1 {
108 sep = "\n\n"
109 idx = strings.Index(raw, sep)
110 advance = 2
111 }
112 if idx == -1 {
113 w.Write([]byte(raw))
114 return nil
115 }
116
117 headerPart := raw[:idx]
118 body := raw[idx+advance:]
119
120 tp := textproto.NewReader(bufio.NewReader(strings.NewReader(headerPart + "\r\n\r\n")))
121 mime, _ := tp.ReadMIMEHeader()
122
123 status := 200
124 if s := mime.Get("Status"); s != "" {
125 code, _, _ := strings.Cut(s, " ")
126 if n := parseIntFast(code); n > 0 {
127 status = n
128 }
129 mime.Del("Status")
130 }
131
132 for k, vs := range mime {
133 for _, v := range vs {
134 w.Header().Add(k, v)
135 }
136 }
137 w.WriteHeader(status)
138 w.Write([]byte(body))
139 return nil
140 }
141
142 func writeRecord(w io.Writer, recType uint8, reqID uint16, content []byte) error {
143 length := len(content)
144 padding := (8 - (length % 8)) % 8
145 hdr := []byte{
146 fcgiVersion, recType,
147 byte(reqID >> 8), byte(reqID),
148 byte(length >> 8), byte(length),
149 byte(padding), 0,
150 }
151 if _, err := w.Write(hdr); err != nil {
152 return err
153 }
154 if len(content) > 0 {
155 if _, err := w.Write(content); err != nil {
156 return err
157 }
158 }
159 if padding > 0 {
160 if _, err := w.Write(make([]byte, padding)); err != nil {
161 return err
162 }
163 }
164 return nil
165 }
166
167 func encodeParams(params map[string]string) []byte {
168 var buf []byte
169 for k, v := range params {
170 buf = appendLen(buf, len(k))
171 buf = appendLen(buf, len(v))
172 buf = append(buf, k...)
173 buf = append(buf, v...)
174 }
175 return buf
176 }
177
178 func appendLen(buf []byte, n int) []byte {
179 if n <= 127 {
180 return append(buf, byte(n))
181 }
182 return append(buf, byte((n>>24)|0x80), byte(n>>16), byte(n>>8), byte(n))
183 }
184
185 func parseIntFast(s string) int {
186 n := 0
187 for i := 0; i < len(s); i++ {
188 c := s[i]
189 if c < '0' || c > '9' {
190 return 0
191 }
192 n = n*10 + int(c-'0')
193 }
194 return n
195 }