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