Mercurial Hosting > d2o
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 } |
