| // Copyright 2011 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // This file implements CGI from the perspective of a child |
| // process. |
| |
| package cgi |
| |
| import ( |
| "bufio" |
| "crypto/tls" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "net/url" |
| "os" |
| "strconv" |
| "strings" |
| ) |
| |
| // Request returns the HTTP request as represented in the current |
| // environment. This assumes the current program is being run |
| // by a web server in a CGI environment. |
| // The returned Request's Body is populated, if applicable. |
| func Request() (*http.Request, error) { |
| r, err := RequestFromMap(envMap(os.Environ())) |
| if err != nil { |
| return nil, err |
| } |
| if r.ContentLength > 0 { |
| r.Body = ioutil.NopCloser(io.LimitReader(os.Stdin, r.ContentLength)) |
| } |
| return r, nil |
| } |
| |
| func envMap(env []string) map[string]string { |
| m := make(map[string]string) |
| for _, kv := range env { |
| if idx := strings.Index(kv, "="); idx != -1 { |
| m[kv[:idx]] = kv[idx+1:] |
| } |
| } |
| return m |
| } |
| |
| // RequestFromMap creates an http.Request from CGI variables. |
| // The returned Request's Body field is not populated. |
| func RequestFromMap(params map[string]string) (*http.Request, error) { |
| r := new(http.Request) |
| r.Method = params["REQUEST_METHOD"] |
| if r.Method == "" { |
| return nil, errors.New("cgi: no REQUEST_METHOD in environment") |
| } |
| |
| r.Proto = params["SERVER_PROTOCOL"] |
| var ok bool |
| r.ProtoMajor, r.ProtoMinor, ok = http.ParseHTTPVersion(r.Proto) |
| if !ok { |
| return nil, errors.New("cgi: invalid SERVER_PROTOCOL version") |
| } |
| |
| r.Close = true |
| r.Trailer = http.Header{} |
| r.Header = http.Header{} |
| |
| r.Host = params["HTTP_HOST"] |
| |
| if lenstr := params["CONTENT_LENGTH"]; lenstr != "" { |
| clen, err := strconv.ParseInt(lenstr, 10, 64) |
| if err != nil { |
| return nil, errors.New("cgi: bad CONTENT_LENGTH in environment: " + lenstr) |
| } |
| r.ContentLength = clen |
| } |
| |
| if ct := params["CONTENT_TYPE"]; ct != "" { |
| r.Header.Set("Content-Type", ct) |
| } |
| |
| // Copy "HTTP_FOO_BAR" variables to "Foo-Bar" Headers |
| for k, v := range params { |
| if !strings.HasPrefix(k, "HTTP_") || k == "HTTP_HOST" { |
| continue |
| } |
| r.Header.Add(strings.ReplaceAll(k[5:], "_", "-"), v) |
| } |
| |
| // TODO: cookies. parsing them isn't exported, though. |
| |
| uriStr := params["REQUEST_URI"] |
| if uriStr == "" { |
| // Fallback to SCRIPT_NAME, PATH_INFO and QUERY_STRING. |
| uriStr = params["SCRIPT_NAME"] + params["PATH_INFO"] |
| s := params["QUERY_STRING"] |
| if s != "" { |
| uriStr += "?" + s |
| } |
| } |
| |
| // There's apparently a de-facto standard for this. |
| // https://docstore.mik.ua/orelly/linux/cgi/ch03_02.htm#ch03-35636 |
| if s := params["HTTPS"]; s == "on" || s == "ON" || s == "1" { |
| r.TLS = &tls.ConnectionState{HandshakeComplete: true} |
| } |
| |
| if r.Host != "" { |
| // Hostname is provided, so we can reasonably construct a URL. |
| rawurl := r.Host + uriStr |
| if r.TLS == nil { |
| rawurl = "http://" + rawurl |
| } else { |
| rawurl = "https://" + rawurl |
| } |
| url, err := url.Parse(rawurl) |
| if err != nil { |
| return nil, errors.New("cgi: failed to parse host and REQUEST_URI into a URL: " + rawurl) |
| } |
| r.URL = url |
| } |
| // Fallback logic if we don't have a Host header or the URL |
| // failed to parse |
| if r.URL == nil { |
| url, err := url.Parse(uriStr) |
| if err != nil { |
| return nil, errors.New("cgi: failed to parse REQUEST_URI into a URL: " + uriStr) |
| } |
| r.URL = url |
| } |
| |
| // Request.RemoteAddr has its port set by Go's standard http |
| // server, so we do here too. |
| remotePort, _ := strconv.Atoi(params["REMOTE_PORT"]) // zero if unset or invalid |
| r.RemoteAddr = net.JoinHostPort(params["REMOTE_ADDR"], strconv.Itoa(remotePort)) |
| |
| return r, nil |
| } |
| |
| // Serve executes the provided Handler on the currently active CGI |
| // request, if any. If there's no current CGI environment |
| // an error is returned. The provided handler may be nil to use |
| // http.DefaultServeMux. |
| func Serve(handler http.Handler) error { |
| req, err := Request() |
| if err != nil { |
| return err |
| } |
| if handler == nil { |
| handler = http.DefaultServeMux |
| } |
| rw := &response{ |
| req: req, |
| header: make(http.Header), |
| bufw: bufio.NewWriter(os.Stdout), |
| } |
| handler.ServeHTTP(rw, req) |
| rw.Write(nil) // make sure a response is sent |
| if err = rw.bufw.Flush(); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| type response struct { |
| req *http.Request |
| header http.Header |
| bufw *bufio.Writer |
| headerSent bool |
| } |
| |
| func (r *response) Flush() { |
| r.bufw.Flush() |
| } |
| |
| func (r *response) Header() http.Header { |
| return r.header |
| } |
| |
| func (r *response) Write(p []byte) (n int, err error) { |
| if !r.headerSent { |
| r.WriteHeader(http.StatusOK) |
| } |
| return r.bufw.Write(p) |
| } |
| |
| func (r *response) WriteHeader(code int) { |
| if r.headerSent { |
| // Note: explicitly using Stderr, as Stdout is our HTTP output. |
| fmt.Fprintf(os.Stderr, "CGI attempted to write header twice on request for %s", r.req.URL) |
| return |
| } |
| r.headerSent = true |
| fmt.Fprintf(r.bufw, "Status: %d %s\r\n", code, http.StatusText(code)) |
| |
| // Set a default Content-Type |
| if _, hasType := r.header["Content-Type"]; !hasType { |
| r.header.Add("Content-Type", "text/html; charset=utf-8") |
| } |
| |
| r.header.Write(r.bufw) |
| r.bufw.WriteString("\r\n") |
| r.bufw.Flush() |
| } |