1// Copyright 2011 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// This file implements the host side of CGI (being the webserver
6// parent process).
7
8// Package cgi implements CGI (Common Gateway Interface) as specified
9// in RFC 3875.
10//
11// Note that using CGI means starting a new process to handle each
12// request, which is typically less efficient than using a
13// long-running server. This package is intended primarily for
14// compatibility with existing systems.
15package cgi
16
17import (
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
36var trailingPort = regexp.MustCompile(`:([0-9]+)$`)
37
38var 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// Handler runs an executable in a subprocess with a CGI environment.
57type Handler struct {
58	Path string // path to the CGI executable
59	Root string // root URI prefix of handler or empty for "/"
60
61	// Dir specifies the CGI executable's working directory.
62	// If Dir is empty, the base directory of Path is used.
63	// If Path has no base directory, the current working
64	// directory is used.
65	Dir string
66
67	Env        []string    // extra environment variables to set, if any, as "key=value"
68	InheritEnv []string    // environment variables to inherit from host, as "key"
69	Logger     *log.Logger // optional log for errors or nil to use log.Print
70	Args       []string    // optional arguments to pass to child process
71	Stderr     io.Writer   // optional stderr for the child process; nil means os.Stderr
72
73	// PathLocationHandler specifies the root http Handler that
74	// should handle internal redirects when the CGI process
75	// returns a Location header value starting with a "/", as
76	// specified in RFC 3875 § 6.3.2. This will likely be
77	// http.DefaultServeMux.
78	//
79	// If nil, a CGI response with a local URI path is instead sent
80	// back to the client and not redirected internally.
81	PathLocationHandler http.Handler
82}
83
84func (h *Handler) stderr() io.Writer {
85	if h.Stderr != nil {
86		return h.Stderr
87	}
88	return os.Stderr
89}
90
91// removeLeadingDuplicates remove leading duplicate in environments.
92// It's possible to override environment like following.
93//
94//	cgi.Handler{
95//	  ...
96//	  Env: []string{"SCRIPT_FILENAME=foo.php"},
97//	}
98func 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] // "key="
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
117func (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		// could not parse ip:port, let's use whole RemoteAddr and leave REMOTE_PORT undefined
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			// See Issue 16405
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	// Copy headers to rw's headers, after we've decided not to
332	// go into handleInternalRedirect, which won't want its rw
333	// headers to have been touched.
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		// And kill the child CGI process so we don't hang on
346		// the deferred cmd.Wait above if the error was just
347		// the client (rw) going away. If it was a read error
348		// (because the child died itself), then the extra
349		// kill of an already-dead process is harmless (the PID
350		// won't be reused until the Wait above).
351		cmd.Process.Kill()
352	}
353}
354
355func (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
363func (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	// TODO: RFC 3875 isn't clear if only GET is supported, but it
371	// suggests so: "Note that any message-body attached to the
372	// request (such as for a POST request) may not be available
373	// to the resource that is the target of the redirect."  We
374	// should do some tests against Apache to see how it handles
375	// POST, HEAD, etc. Does the internal redirect get the same
376	// method or just GET? What about incoming headers?
377	// (e.g. Cookies) Which headers, if any, are copied into the
378	// second request?
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
393func 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		// Maybe not part of the CGI 'spec' but would mess up
401		// the environment in any case, as Go represents the
402		// environment as a slice of "key=value" strings.
403		return '_'
404	}
405	// TODO: other transformations in spec or practice?
406	return r
407}
408
409var testHookStartProcess func(*os.Process) // nil except for some tests
410