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