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 CGI from the perspective of a child
6// process.
7
8package cgi
9
10import (
11	"bufio"
12	"crypto/tls"
13	"errors"
14	"fmt"
15	"io"
16	"net"
17	"net/http"
18	"net/url"
19	"os"
20	"strconv"
21	"strings"
22)
23
24// Request returns the HTTP request as represented in the current
25// environment. This assumes the current program is being run
26// by a web server in a CGI environment.
27// The returned Request's Body is populated, if applicable.
28func Request() (*http.Request, error) {
29	r, err := RequestFromMap(envMap(os.Environ()))
30	if err != nil {
31		return nil, err
32	}
33	if r.ContentLength > 0 {
34		r.Body = io.NopCloser(io.LimitReader(os.Stdin, r.ContentLength))
35	}
36	return r, nil
37}
38
39func envMap(env []string) map[string]string {
40	m := make(map[string]string)
41	for _, kv := range env {
42		if k, v, ok := strings.Cut(kv, "="); ok {
43			m[k] = v
44		}
45	}
46	return m
47}
48
49// RequestFromMap creates an [http.Request] from CGI variables.
50// The returned Request's Body field is not populated.
51func RequestFromMap(params map[string]string) (*http.Request, error) {
52	r := new(http.Request)
53	r.Method = params["REQUEST_METHOD"]
54	if r.Method == "" {
55		return nil, errors.New("cgi: no REQUEST_METHOD in environment")
56	}
57
58	r.Proto = params["SERVER_PROTOCOL"]
59	var ok bool
60	r.ProtoMajor, r.ProtoMinor, ok = http.ParseHTTPVersion(r.Proto)
61	if !ok {
62		return nil, errors.New("cgi: invalid SERVER_PROTOCOL version")
63	}
64
65	r.Close = true
66	r.Trailer = http.Header{}
67	r.Header = http.Header{}
68
69	r.Host = params["HTTP_HOST"]
70
71	if lenstr := params["CONTENT_LENGTH"]; lenstr != "" {
72		clen, err := strconv.ParseInt(lenstr, 10, 64)
73		if err != nil {
74			return nil, errors.New("cgi: bad CONTENT_LENGTH in environment: " + lenstr)
75		}
76		r.ContentLength = clen
77	}
78
79	if ct := params["CONTENT_TYPE"]; ct != "" {
80		r.Header.Set("Content-Type", ct)
81	}
82
83	// Copy "HTTP_FOO_BAR" variables to "Foo-Bar" Headers
84	for k, v := range params {
85		if k == "HTTP_HOST" {
86			continue
87		}
88		if after, found := strings.CutPrefix(k, "HTTP_"); found {
89			r.Header.Add(strings.ReplaceAll(after, "_", "-"), v)
90		}
91	}
92
93	uriStr := params["REQUEST_URI"]
94	if uriStr == "" {
95		// Fallback to SCRIPT_NAME, PATH_INFO and QUERY_STRING.
96		uriStr = params["SCRIPT_NAME"] + params["PATH_INFO"]
97		s := params["QUERY_STRING"]
98		if s != "" {
99			uriStr += "?" + s
100		}
101	}
102
103	// There's apparently a de-facto standard for this.
104	// https://web.archive.org/web/20170105004655/http://docstore.mik.ua/orelly/linux/cgi/ch03_02.htm#ch03-35636
105	if s := params["HTTPS"]; s == "on" || s == "ON" || s == "1" {
106		r.TLS = &tls.ConnectionState{HandshakeComplete: true}
107	}
108
109	if r.Host != "" {
110		// Hostname is provided, so we can reasonably construct a URL.
111		rawurl := r.Host + uriStr
112		if r.TLS == nil {
113			rawurl = "http://" + rawurl
114		} else {
115			rawurl = "https://" + rawurl
116		}
117		url, err := url.Parse(rawurl)
118		if err != nil {
119			return nil, errors.New("cgi: failed to parse host and REQUEST_URI into a URL: " + rawurl)
120		}
121		r.URL = url
122	}
123	// Fallback logic if we don't have a Host header or the URL
124	// failed to parse
125	if r.URL == nil {
126		url, err := url.Parse(uriStr)
127		if err != nil {
128			return nil, errors.New("cgi: failed to parse REQUEST_URI into a URL: " + uriStr)
129		}
130		r.URL = url
131	}
132
133	// Request.RemoteAddr has its port set by Go's standard http
134	// server, so we do here too.
135	remotePort, _ := strconv.Atoi(params["REMOTE_PORT"]) // zero if unset or invalid
136	r.RemoteAddr = net.JoinHostPort(params["REMOTE_ADDR"], strconv.Itoa(remotePort))
137
138	return r, nil
139}
140
141// Serve executes the provided [Handler] on the currently active CGI
142// request, if any. If there's no current CGI environment
143// an error is returned. The provided handler may be nil to use
144// [http.DefaultServeMux].
145func Serve(handler http.Handler) error {
146	req, err := Request()
147	if err != nil {
148		return err
149	}
150	if req.Body == nil {
151		req.Body = http.NoBody
152	}
153	if handler == nil {
154		handler = http.DefaultServeMux
155	}
156	rw := &response{
157		req:    req,
158		header: make(http.Header),
159		bufw:   bufio.NewWriter(os.Stdout),
160	}
161	handler.ServeHTTP(rw, req)
162	rw.Write(nil) // make sure a response is sent
163	if err = rw.bufw.Flush(); err != nil {
164		return err
165	}
166	return nil
167}
168
169type response struct {
170	req            *http.Request
171	header         http.Header
172	code           int
173	wroteHeader    bool
174	wroteCGIHeader bool
175	bufw           *bufio.Writer
176}
177
178func (r *response) Flush() {
179	r.bufw.Flush()
180}
181
182func (r *response) Header() http.Header {
183	return r.header
184}
185
186func (r *response) Write(p []byte) (n int, err error) {
187	if !r.wroteHeader {
188		r.WriteHeader(http.StatusOK)
189	}
190	if !r.wroteCGIHeader {
191		r.writeCGIHeader(p)
192	}
193	return r.bufw.Write(p)
194}
195
196func (r *response) WriteHeader(code int) {
197	if r.wroteHeader {
198		// Note: explicitly using Stderr, as Stdout is our HTTP output.
199		fmt.Fprintf(os.Stderr, "CGI attempted to write header twice on request for %s", r.req.URL)
200		return
201	}
202	r.wroteHeader = true
203	r.code = code
204}
205
206// writeCGIHeader finalizes the header sent to the client and writes it to the output.
207// p is not written by writeHeader, but is the first chunk of the body
208// that will be written. It is sniffed for a Content-Type if none is
209// set explicitly.
210func (r *response) writeCGIHeader(p []byte) {
211	if r.wroteCGIHeader {
212		return
213	}
214	r.wroteCGIHeader = true
215	fmt.Fprintf(r.bufw, "Status: %d %s\r\n", r.code, http.StatusText(r.code))
216	if _, hasType := r.header["Content-Type"]; !hasType {
217		r.header.Set("Content-Type", http.DetectContentType(p))
218	}
219	r.header.Write(r.bufw)
220	r.bufw.WriteString("\r\n")
221	r.bufw.Flush()
222}
223