github.com/useflyent/fhttp@v0.0.0-20211004035111-333f430cfbbf/cgi/child.go (about) 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 8 package cgi 9 10 import ( 11 "bufio" 12 "crypto/tls" 13 "errors" 14 "fmt" 15 "io" 16 "net" 17 "net/url" 18 "os" 19 "strconv" 20 "strings" 21 22 http "github.com/useflyent/fhttp" 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. 29 func 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 = io.NopCloser(io.LimitReader(os.Stdin, r.ContentLength)) 36 } 37 return r, nil 38 } 39 40 func 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. 52 func 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.ReplaceAll(k[5:], "_", "-"), v) 90 } 91 92 uriStr := params["REQUEST_URI"] 93 if uriStr == "" { 94 // Fallback to SCRIPT_NAME, PATH_INFO and QUERY_STRING. 95 uriStr = params["SCRIPT_NAME"] + params["PATH_INFO"] 96 s := params["QUERY_STRING"] 97 if s != "" { 98 uriStr += "?" + s 99 } 100 } 101 102 // There's apparently a de-facto standard for this. 103 // https://web.archive.org/web/20170105004655/http://docstore.mik.ua/orelly/linux/cgi/ch03_02.htm#ch03-35636 104 if s := params["HTTPS"]; s == "on" || s == "ON" || s == "1" { 105 r.TLS = &tls.ConnectionState{HandshakeComplete: true} 106 } 107 108 if r.Host != "" { 109 // Hostname is provided, so we can reasonably construct a URL. 110 rawurl := r.Host + uriStr 111 if r.TLS == nil { 112 rawurl = "http://" + rawurl 113 } else { 114 rawurl = "https://" + rawurl 115 } 116 url, err := url.Parse(rawurl) 117 if err != nil { 118 return nil, errors.New("cgi: failed to parse host and REQUEST_URI into a URL: " + rawurl) 119 } 120 r.URL = url 121 } 122 // Fallback logic if we don't have a Host header or the URL 123 // failed to parse 124 if r.URL == nil { 125 url, err := url.Parse(uriStr) 126 if err != nil { 127 return nil, errors.New("cgi: failed to parse REQUEST_URI into a URL: " + uriStr) 128 } 129 r.URL = url 130 } 131 132 // Request.RemoteAddr has its port set by Go's standard http 133 // server, so we do here too. 134 remotePort, _ := strconv.Atoi(params["REMOTE_PORT"]) // zero if unset or invalid 135 r.RemoteAddr = net.JoinHostPort(params["REMOTE_ADDR"], strconv.Itoa(remotePort)) 136 137 return r, nil 138 } 139 140 // Serve executes the provided Handler on the currently active CGI 141 // request, if any. If there's no current CGI environment 142 // an error is returned. The provided handler may be nil to use 143 // http.DefaultServeMux. 144 func Serve(handler http.Handler) error { 145 req, err := Request() 146 if err != nil { 147 return err 148 } 149 if req.Body == nil { 150 req.Body = http.NoBody 151 } 152 if handler == nil { 153 handler = http.DefaultServeMux 154 } 155 rw := &response{ 156 req: req, 157 header: make(http.Header), 158 bufw: bufio.NewWriter(os.Stdout), 159 } 160 handler.ServeHTTP(rw, req) 161 rw.Write(nil) // make sure a response is sent 162 if err = rw.bufw.Flush(); err != nil { 163 return err 164 } 165 return nil 166 } 167 168 type response struct { 169 req *http.Request 170 header http.Header 171 code int 172 wroteHeader bool 173 wroteCGIHeader bool 174 bufw *bufio.Writer 175 } 176 177 func (r *response) Flush() { 178 r.bufw.Flush() 179 } 180 181 func (r *response) Header() http.Header { 182 return r.header 183 } 184 185 func (r *response) Write(p []byte) (n int, err error) { 186 if !r.wroteHeader { 187 r.WriteHeader(http.StatusOK) 188 } 189 if !r.wroteCGIHeader { 190 r.writeCGIHeader(p) 191 } 192 return r.bufw.Write(p) 193 } 194 195 func (r *response) WriteHeader(code int) { 196 if r.wroteHeader { 197 // Note: explicitly using Stderr, as Stdout is our HTTP output. 198 fmt.Fprintf(os.Stderr, "CGI attempted to write header twice on request for %s", r.req.URL) 199 return 200 } 201 r.wroteHeader = true 202 r.code = code 203 } 204 205 // writeCGIHeader finalizes the header sent to the client and writes it to the output. 206 // p is not written by writeHeader, but is the first chunk of the body 207 // that will be written. It is sniffed for a Content-Type if none is 208 // set explicitly. 209 func (r *response) writeCGIHeader(p []byte) { 210 if r.wroteCGIHeader { 211 return 212 } 213 r.wroteCGIHeader = true 214 fmt.Fprintf(r.bufw, "Status: %d %s\r\n", r.code, http.StatusText(r.code)) 215 if _, hasType := r.header["Content-Type"]; !hasType { 216 r.header.Set("Content-Type", http.DetectContentType(p)) 217 } 218 r.header.Write(r.bufw) 219 r.bufw.WriteString("\r\n") 220 r.bufw.Flush() 221 }