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_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 hostDomain, _, err := net.SplitHostPort(req.Host); err == nil {
161 env = append(env, "SERVER_NAME="+hostDomain)
162 } else {
163 env = append(env, "SERVER_NAME="+req.Host)
164 }
165
166 if req.TLS != nil {
167 env = append(env, "HTTPS=on")
168 }
169
170 for k, v := range req.Header {
171 k = strings.Map(upperCaseAndUnderscore, k)
172 if k == "PROXY" {
173
174 continue
175 }
176 joinStr := ", "
177 if k == "COOKIE" {
178 joinStr = "; "
179 }
180 env = append(env, "HTTP_"+k+"="+strings.Join(v, joinStr))
181 }
182
183 if req.ContentLength > 0 {
184 env = append(env, fmt.Sprintf("CONTENT_LENGTH=%d", req.ContentLength))
185 }
186 if ctype := req.Header.Get("Content-Type"); ctype != "" {
187 env = append(env, "CONTENT_TYPE="+ctype)
188 }
189
190 envPath := os.Getenv("PATH")
191 if envPath == "" {
192 envPath = "/bin:/usr/bin:/usr/ucb:/usr/bsd:/usr/local/bin"
193 }
194 env = append(env, "PATH="+envPath)
195
196 for _, e := range h.InheritEnv {
197 if v := os.Getenv(e); v != "" {
198 env = append(env, e+"="+v)
199 }
200 }
201
202 for _, e := range osDefaultInheritEnv {
203 if v := os.Getenv(e); v != "" {
204 env = append(env, e+"="+v)
205 }
206 }
207
208 if h.Env != nil {
209 env = append(env, h.Env...)
210 }
211
212 env = removeLeadingDuplicates(env)
213
214 var cwd, path string
215 if h.Dir != "" {
216 path = h.Path
217 cwd = h.Dir
218 } else {
219 cwd, path = filepath.Split(h.Path)
220 }
221 if cwd == "" {
222 cwd = "."
223 }
224
225 internalError := func(err error) {
226 rw.WriteHeader(http.StatusInternalServerError)
227 h.printf("CGI error: %v", err)
228 }
229
230 cmd := &exec.Cmd{
231 Path: path,
232 Args: append([]string{h.Path}, h.Args...),
233 Dir: cwd,
234 Env: env,
235 Stderr: h.stderr(),
236 }
237 if req.ContentLength != 0 {
238 cmd.Stdin = req.Body
239 }
240 stdoutRead, err := cmd.StdoutPipe()
241 if err != nil {
242 internalError(err)
243 return
244 }
245
246 err = cmd.Start()
247 if err != nil {
248 internalError(err)
249 return
250 }
251 if hook := testHookStartProcess; hook != nil {
252 hook(cmd.Process)
253 }
254 defer cmd.Wait()
255 defer stdoutRead.Close()
256
257 linebody := bufio.NewReaderSize(stdoutRead, 1024)
258 headers := make(http.Header)
259 statusCode := 0
260 headerLines := 0
261 sawBlankLine := false
262 for {
263 line, isPrefix, err := linebody.ReadLine()
264 if isPrefix {
265 rw.WriteHeader(http.StatusInternalServerError)
266 h.printf("cgi: long header line from subprocess.")
267 return
268 }
269 if err == io.EOF {
270 break
271 }
272 if err != nil {
273 rw.WriteHeader(http.StatusInternalServerError)
274 h.printf("cgi: error reading headers: %v", err)
275 return
276 }
277 if len(line) == 0 {
278 sawBlankLine = true
279 break
280 }
281 headerLines++
282 header, val, ok := strings.Cut(string(line), ":")
283 if !ok {
284 h.printf("cgi: bogus header line: %s", string(line))
285 continue
286 }
287 if !httpguts.ValidHeaderFieldName(header) {
288 h.printf("cgi: invalid header name: %q", header)
289 continue
290 }
291 val = textproto.TrimString(val)
292 switch {
293 case header == "Status":
294 if len(val) < 3 {
295 h.printf("cgi: bogus status (short): %q", val)
296 return
297 }
298 code, err := strconv.Atoi(val[0:3])
299 if err != nil {
300 h.printf("cgi: bogus status: %q", val)
301 h.printf("cgi: line was %q", line)
302 return
303 }
304 statusCode = code
305 default:
306 headers.Add(header, val)
307 }
308 }
309 if headerLines == 0 || !sawBlankLine {
310 rw.WriteHeader(http.StatusInternalServerError)
311 h.printf("cgi: no headers")
312 return
313 }
314
315 if loc := headers.Get("Location"); loc != "" {
316 if strings.HasPrefix(loc, "/") && h.PathLocationHandler != nil {
317 h.handleInternalRedirect(rw, req, loc)
318 return
319 }
320 if statusCode == 0 {
321 statusCode = http.StatusFound
322 }
323 }
324
325 if statusCode == 0 && headers.Get("Content-Type") == "" {
326 rw.WriteHeader(http.StatusInternalServerError)
327 h.printf("cgi: missing required Content-Type in headers")
328 return
329 }
330
331 if statusCode == 0 {
332 statusCode = http.StatusOK
333 }
334
335
336
337
338 for k, vv := range headers {
339 for _, v := range vv {
340 rw.Header().Add(k, v)
341 }
342 }
343
344 rw.WriteHeader(statusCode)
345
346 _, err = io.Copy(rw, linebody)
347 if err != nil {
348 h.printf("cgi: copy error: %v", err)
349
350
351
352
353
354
355 cmd.Process.Kill()
356 }
357 }
358
359 func (h *Handler) printf(format string, v ...any) {
360 if h.Logger != nil {
361 h.Logger.Printf(format, v...)
362 } else {
363 log.Printf(format, v...)
364 }
365 }
366
367 func (h *Handler) handleInternalRedirect(rw http.ResponseWriter, req *http.Request, path string) {
368 url, err := req.URL.Parse(path)
369 if err != nil {
370 rw.WriteHeader(http.StatusInternalServerError)
371 h.printf("cgi: error resolving local URI path %q: %v", path, err)
372 return
373 }
374
375
376
377
378
379
380
381
382
383 newReq := &http.Request{
384 Method: "GET",
385 URL: url,
386 Proto: "HTTP/1.1",
387 ProtoMajor: 1,
388 ProtoMinor: 1,
389 Header: make(http.Header),
390 Host: url.Host,
391 RemoteAddr: req.RemoteAddr,
392 TLS: req.TLS,
393 }
394 h.PathLocationHandler.ServeHTTP(rw, newReq)
395 }
396
397 func upperCaseAndUnderscore(r rune) rune {
398 switch {
399 case r >= 'a' && r <= 'z':
400 return r - ('a' - 'A')
401 case r == '-':
402 return '_'
403 case r == '=':
404
405
406
407 return '_'
408 }
409
410 return r
411 }
412
413 var testHookStartProcess func(*os.Process)
414
View as plain text