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