1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package cgi
16
17 import (
18 "bufio"
19 "fmt"
20 "io"
21 "log"
22 "net"
23 "net/http"
24 "net/textproto"
25 "os"
26 "os/exec"
27 "path/filepath"
28 "regexp"
29 "runtime"
30 "strconv"
31 "strings"
32
33 "golang.org/x/net/http/httpguts"
34 )
35
36 var trailingPort = regexp.MustCompile(`:([0-9]+)$`)
37
38 var osDefaultInheritEnv = func() []string {
39 switch runtime.GOOS {
40 case "darwin", "ios":
41 return []string{"DYLD_LIBRARY_PATH"}
42 case "linux", "freebsd", "netbsd", "openbsd":
43 return []string{"LD_LIBRARY_PATH"}
44 case "hpux":
45 return []string{"LD_LIBRARY_PATH", "SHLIB_PATH"}
46 case "irix":
47 return []string{"LD_LIBRARY_PATH", "LD_LIBRARYN32_PATH", "LD_LIBRARY64_PATH"}
48 case "illumos", "solaris":
49 return []string{"LD_LIBRARY_PATH", "LD_LIBRARY_PATH_32", "LD_LIBRARY_PATH_64"}
50 case "windows":
51 return []string{"SystemRoot", "COMSPEC", "PATHEXT", "WINDIR"}
52 }
53 return nil
54 }()
55
56
57 type Handler struct {
58 Path string
59 Root string
60
61
62
63
64
65 Dir string
66
67 Env []string
68 InheritEnv []string
69 Logger *log.Logger
70 Args []string
71 Stderr io.Writer
72
73
74
75
76
77
78
79
80
81 PathLocationHandler http.Handler
82 }
83
84 func (h *Handler) stderr() io.Writer {
85 if h.Stderr != nil {
86 return h.Stderr
87 }
88 return os.Stderr
89 }
90
91
92
93
94
95
96
97
98 func removeLeadingDuplicates(env []string) (ret []string) {
99 for i, e := range env {
100 found := false
101 if eq := strings.IndexByte(e, '='); eq != -1 {
102 keq := e[:eq+1]
103 for _, e2 := range env[i+1:] {
104 if strings.HasPrefix(e2, keq) {
105 found = true
106 break
107 }
108 }
109 }
110 if !found {
111 ret = append(ret, e)
112 }
113 }
114 return
115 }
116
117 func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
118 root := h.Root
119 if root == "" {
120 root = "/"
121 }
122
123 if len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" {
124 rw.WriteHeader(http.StatusBadRequest)
125 rw.Write([]byte("Chunked request bodies are not supported by CGI."))
126 return
127 }
128
129 pathInfo := req.URL.Path
130 if root != "/" && strings.HasPrefix(pathInfo, root) {
131 pathInfo = pathInfo[len(root):]
132 }
133
134 port := "80"
135 if matches := trailingPort.FindStringSubmatch(req.Host); len(matches) != 0 {
136 port = matches[1]
137 }
138
139 env := []string{
140 "SERVER_SOFTWARE=go",
141 "SERVER_NAME=" + req.Host,
142 "SERVER_PROTOCOL=HTTP/1.1",
143 "HTTP_HOST=" + req.Host,
144 "GATEWAY_INTERFACE=CGI/1.1",
145 "REQUEST_METHOD=" + req.Method,
146 "QUERY_STRING=" + req.URL.RawQuery,
147 "REQUEST_URI=" + req.URL.RequestURI(),
148 "PATH_INFO=" + pathInfo,
149 "SCRIPT_NAME=" + root,
150 "SCRIPT_FILENAME=" + h.Path,
151 "SERVER_PORT=" + port,
152 }
153
154 if remoteIP, remotePort, err := net.SplitHostPort(req.RemoteAddr); err == nil {
155 env = append(env, "REMOTE_ADDR="+remoteIP, "REMOTE_HOST="+remoteIP, "REMOTE_PORT="+remotePort)
156 } else {
157
158 env = append(env, "REMOTE_ADDR="+req.RemoteAddr, "REMOTE_HOST="+req.RemoteAddr)
159 }
160
161 if req.TLS != nil {
162 env = append(env, "HTTPS=on")
163 }
164
165 for k, v := range req.Header {
166 k = strings.Map(upperCaseAndUnderscore, k)
167 if k == "PROXY" {
168
169 continue
170 }
171 joinStr := ", "
172 if k == "COOKIE" {
173 joinStr = "; "
174 }
175 env = append(env, "HTTP_"+k+"="+strings.Join(v, joinStr))
176 }
177
178 if req.ContentLength > 0 {
179 env = append(env, fmt.Sprintf("CONTENT_LENGTH=%d", req.ContentLength))
180 }
181 if ctype := req.Header.Get("Content-Type"); ctype != "" {
182 env = append(env, "CONTENT_TYPE="+ctype)
183 }
184
185 envPath := os.Getenv("PATH")
186 if envPath == "" {
187 envPath = "/bin:/usr/bin:/usr/ucb:/usr/bsd:/usr/local/bin"
188 }
189 env = append(env, "PATH="+envPath)
190
191 for _, e := range h.InheritEnv {
192 if v := os.Getenv(e); v != "" {
193 env = append(env, e+"="+v)
194 }
195 }
196
197 for _, e := range osDefaultInheritEnv {
198 if v := os.Getenv(e); v != "" {
199 env = append(env, e+"="+v)
200 }
201 }
202
203 if h.Env != nil {
204 env = append(env, h.Env...)
205 }
206
207 env = removeLeadingDuplicates(env)
208
209 var cwd, path string
210 if h.Dir != "" {
211 path = h.Path
212 cwd = h.Dir
213 } else {
214 cwd, path = filepath.Split(h.Path)
215 }
216 if cwd == "" {
217 cwd = "."
218 }
219
220 internalError := func(err error) {
221 rw.WriteHeader(http.StatusInternalServerError)
222 h.printf("CGI error: %v", err)
223 }
224
225 cmd := &exec.Cmd{
226 Path: path,
227 Args: append([]string{h.Path}, h.Args...),
228 Dir: cwd,
229 Env: env,
230 Stderr: h.stderr(),
231 }
232 if req.ContentLength != 0 {
233 cmd.Stdin = req.Body
234 }
235 stdoutRead, err := cmd.StdoutPipe()
236 if err != nil {
237 internalError(err)
238 return
239 }
240
241 err = cmd.Start()
242 if err != nil {
243 internalError(err)
244 return
245 }
246 if hook := testHookStartProcess; hook != nil {
247 hook(cmd.Process)
248 }
249 defer cmd.Wait()
250 defer stdoutRead.Close()
251
252 linebody := bufio.NewReaderSize(stdoutRead, 1024)
253 headers := make(http.Header)
254 statusCode := 0
255 headerLines := 0
256 sawBlankLine := false
257 for {
258 line, isPrefix, err := linebody.ReadLine()
259 if isPrefix {
260 rw.WriteHeader(http.StatusInternalServerError)
261 h.printf("cgi: long header line from subprocess.")
262 return
263 }
264 if err == io.EOF {
265 break
266 }
267 if err != nil {
268 rw.WriteHeader(http.StatusInternalServerError)
269 h.printf("cgi: error reading headers: %v", err)
270 return
271 }
272 if len(line) == 0 {
273 sawBlankLine = true
274 break
275 }
276 headerLines++
277 header, val, ok := strings.Cut(string(line), ":")
278 if !ok {
279 h.printf("cgi: bogus header line: %s", string(line))
280 continue
281 }
282 if !httpguts.ValidHeaderFieldName(header) {
283 h.printf("cgi: invalid header name: %q", header)
284 continue
285 }
286 val = textproto.TrimString(val)
287 switch {
288 case header == "Status":
289 if len(val) < 3 {
290 h.printf("cgi: bogus status (short): %q", val)
291 return
292 }
293 code, err := strconv.Atoi(val[0:3])
294 if err != nil {
295 h.printf("cgi: bogus status: %q", val)
296 h.printf("cgi: line was %q", line)
297 return
298 }
299 statusCode = code
300 default:
301 headers.Add(header, val)
302 }
303 }
304 if headerLines == 0 || !sawBlankLine {
305 rw.WriteHeader(http.StatusInternalServerError)
306 h.printf("cgi: no headers")
307 return
308 }
309
310 if loc := headers.Get("Location"); loc != "" {
311 if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
312 h.handleInternalRedirect(rw, req, loc)
313 return
314 }
315 if statusCode == 0 {
316 statusCode = http.StatusFound
317 }
318 }
319
320 if statusCode == 0 && headers.Get("Content-Type") == "" {
321 rw.WriteHeader(http.StatusInternalServerError)
322 h.printf("cgi: missing required Content-Type in headers")
323 return
324 }
325
326 if statusCode == 0 {
327 statusCode = http.StatusOK
328 }
329
330
331
332
333 for k, vv := range headers {
334 for _, v := range vv {
335 rw.Header().Add(k, v)
336 }
337 }
338
339 rw.WriteHeader(statusCode)
340
341 _, err = io.Copy(rw, linebody)
342 if err != nil {
343 h.printf("cgi: copy error: %v", err)
344
345
346
347
348
349
350 cmd.Process.Kill()
351 }
352 }
353
354 func (h *Handler) printf(format string, v ...any) {
355 if h.Logger != nil {
356 h.Logger.Printf(format, v...)
357 } else {
358 log.Printf(format, v...)
359 }
360 }
361
362 func (h *Handler) handleInternalRedirect(rw http.ResponseWriter, req *http.Request, path string) {
363 url, err := req.URL.Parse(path)
364 if err != nil {
365 rw.WriteHeader(http.StatusInternalServerError)
366 h.printf("cgi: error resolving local URI path %q: %v", path, err)
367 return
368 }
369
370
371
372
373
374
375
376
377
378 newReq := &http.Request{
379 Method: "GET",
380 URL: url,
381 Proto: "HTTP/1.1",
382 ProtoMajor: 1,
383 ProtoMinor: 1,
384 Header: make(http.Header),
385 Host: url.Host,
386 RemoteAddr: req.RemoteAddr,
387 TLS: req.TLS,
388 }
389 h.PathLocationHandler.ServeHTTP(rw, newReq)
390 }
391
392 func upperCaseAndUnderscore(r rune) rune {
393 switch {
394 case r >= 'a' && r <= 'z':
395 return r - ('a' - 'A')
396 case r == '-':
397 return '_'
398 case r == '=':
399
400
401
402 return '_'
403 }
404
405 return r
406 }
407
408 var testHookStartProcess func(*os.Process)
409
View as plain text