]> Cypherpunks repositories - gostls13.git/commitdiff
cgi: child support (e.g. Go CGI under Apache)
authorBrad Fitzpatrick <bradfitz@golang.org>
Tue, 8 Mar 2011 16:01:19 +0000 (08:01 -0800)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 8 Mar 2011 16:01:19 +0000 (08:01 -0800)
The http/cgi package now supports both being
a CGI host or being a CGI child process.

R=rsc, adg, bradfitzwork
CC=golang-dev
https://golang.org/cl/4245070

src/pkg/http/cgi/Makefile
src/pkg/http/cgi/child.go [new file with mode: 0644]
src/pkg/http/cgi/child_test.go [new file with mode: 0644]
src/pkg/http/cgi/host.go [moved from src/pkg/http/cgi/cgi.go with 86% similarity]
src/pkg/http/cgi/host_test.go [moved from src/pkg/http/cgi/cgi_test.go with 92% similarity]
src/pkg/http/cgi/matryoshka_test.go [new file with mode: 0644]
src/pkg/http/cgi/testdata/test.cgi
src/pkg/http/server.go

index 02f6cfc9e754d24f12ed649815440c53a4262cdb..19b1039c260f9f3ec5448f9b4b1d03d6427d4b9f 100644 (file)
@@ -6,6 +6,7 @@ include ../../../Make.inc
 
 TARG=http/cgi
 GOFILES=\
-       cgi.go\
+       child.go\
+       host.go\
 
 include ../../../Make.pkg
diff --git a/src/pkg/http/cgi/child.go b/src/pkg/http/cgi/child.go
new file mode 100644 (file)
index 0000000..50f90e5
--- /dev/null
@@ -0,0 +1,202 @@
+// 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"
+       "fmt"
+       "http"
+       "io"
+       "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.
+func Request() (*http.Request, os.Error) {
+       return requestFromEnvironment(envMap(os.Environ()))
+}
+
+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
+}
+
+// These environment variables are manually copied into Request
+var skipHeader = map[string]bool{
+       "HTTP_HOST":       true,
+       "HTTP_REFERER":    true,
+       "HTTP_USER_AGENT": true,
+}
+
+func requestFromEnvironment(env map[string]string) (*http.Request, os.Error) {
+       r := new(http.Request)
+       r.Method = env["REQUEST_METHOD"]
+       if r.Method == "" {
+               return nil, os.NewError("cgi: no REQUEST_METHOD in environment")
+       }
+       r.Close = true
+       r.Trailer = http.Header{}
+       r.Header = http.Header{}
+
+       r.Host = env["HTTP_HOST"]
+       r.Referer = env["HTTP_REFERER"]
+       r.UserAgent = env["HTTP_USER_AGENT"]
+
+       // CGI doesn't allow chunked requests, so these should all be accurate:
+       r.Proto = "HTTP/1.0"
+       r.ProtoMajor = 1
+       r.ProtoMinor = 0
+       r.TransferEncoding = nil
+
+       if lenstr := env["CONTENT_LENGTH"]; lenstr != "" {
+               clen, err := strconv.Atoi64(lenstr)
+               if err != nil {
+                       return nil, os.NewError("cgi: bad CONTENT_LENGTH in environment: " + lenstr)
+               }
+               r.ContentLength = clen
+               r.Body = nopCloser{io.LimitReader(os.Stdin, clen)}
+       }
+
+       // Copy "HTTP_FOO_BAR" variables to "Foo-Bar" Headers
+       for k, v := range env {
+               if !strings.HasPrefix(k, "HTTP_") || skipHeader[k] {
+                       continue
+               }
+               r.Header.Add(strings.Replace(k[5:], "_", "-", -1), v)
+       }
+
+       // TODO: cookies.  parsing them isn't exported, though.
+
+       if r.Host != "" {
+               // Hostname is provided, so we can reasonably construct a URL,
+               // even if we have to assume 'http' for the scheme.
+               r.RawURL = "http://" + r.Host + env["REQUEST_URI"]
+               url, err := http.ParseURL(r.RawURL)
+               if err != nil {
+                       return nil, os.NewError("cgi: failed to parse host and REQUEST_URI into a URL: " + r.RawURL)
+               }
+               r.URL = url
+       }
+       // Fallback logic if we don't have a Host header or the URL
+       // failed to parse
+       if r.URL == nil {
+               r.RawURL = env["REQUEST_URI"]
+               url, err := http.ParseURL(r.RawURL)
+               if err != nil {
+                       return nil, os.NewError("cgi: failed to parse REQUEST_URI into a URL: " + r.RawURL)
+               }
+               r.URL = url
+       }
+       return r, nil
+}
+
+// TODO: move this to ioutil or something.  It's copy/pasted way too often.
+type nopCloser struct {
+       io.Reader
+}
+
+func (nopCloser) Close() os.Error { return 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) os.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)
+       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) RemoteAddr() string {
+       return os.Getenv("REMOTE_ADDR")
+}
+
+func (r *response) SetHeader(k, v string) {
+       if v == "" {
+               r.header.Del(k)
+       } else {
+               r.header.Set(k, v)
+       }
+}
+
+func (r *response) Write(p []byte) (n int, err os.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")
+       }
+
+       // TODO: add a method on http.Header to write itself to an io.Writer?
+       // This is duplicated code.
+       for k, vv := range r.header {
+               for _, v := range vv {
+                       v = strings.Replace(v, "\n", "", -1)
+                       v = strings.Replace(v, "\r", "", -1)
+                       v = strings.TrimSpace(v)
+                       fmt.Fprintf(r.bufw, "%s: %s\r\n", k, v)
+               }
+       }
+       r.bufw.Write([]byte("\r\n"))
+       r.bufw.Flush()
+}
+
+func (r *response) UsingTLS() bool {
+       // There's apparently a de-facto standard for this.
+       // http://docstore.mik.ua/orelly/linux/cgi/ch03_02.htm#ch03-35636
+       if s := os.Getenv("HTTPS"); s == "on" || s == "ON" || s == "1" {
+               return true
+       }
+       return false
+}
diff --git a/src/pkg/http/cgi/child_test.go b/src/pkg/http/cgi/child_test.go
new file mode 100644 (file)
index 0000000..db0e09c
--- /dev/null
@@ -0,0 +1,83 @@
+// 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.
+
+// Tests for CGI (the child process perspective)
+
+package cgi
+
+import (
+       "testing"
+)
+
+func TestRequest(t *testing.T) {
+       env := map[string]string{
+               "REQUEST_METHOD":  "GET",
+               "HTTP_HOST":       "example.com",
+               "HTTP_REFERER":    "elsewhere",
+               "HTTP_USER_AGENT": "goclient",
+               "HTTP_FOO_BAR":    "baz",
+               "REQUEST_URI":     "/path?a=b",
+               "CONTENT_LENGTH":  "123",
+       }
+       req, err := requestFromEnvironment(env)
+       if err != nil {
+               t.Fatalf("requestFromEnvironment: %v", err)
+       }
+       if g, e := req.UserAgent, "goclient"; e != g {
+               t.Errorf("expected UserAgent %q; got %q", e, g)
+       }
+       if g, e := req.Method, "GET"; e != g {
+               t.Errorf("expected Method %q; got %q", e, g)
+       }
+       if g, e := req.Header.Get("User-Agent"), ""; e != g {
+               // Tests that we don't put recognized headers in the map
+               t.Errorf("expected User-Agent %q; got %q", e, g)
+       }
+       if g, e := req.ContentLength, int64(123); e != g {
+               t.Errorf("expected ContentLength %d; got %d", e, g)
+       }
+       if g, e := req.Referer, "elsewhere"; e != g {
+               t.Errorf("expected Referer %q; got %q", e, g)
+       }
+       if req.Header == nil {
+               t.Fatalf("unexpected nil Header")
+       }
+       if g, e := req.Header.Get("Foo-Bar"), "baz"; e != g {
+               t.Errorf("expected Foo-Bar %q; got %q", e, g)
+       }
+       if g, e := req.RawURL, "http://example.com/path?a=b"; e != g {
+               t.Errorf("expected RawURL %q; got %q", e, g)
+       }
+       if g, e := req.URL.String(), "http://example.com/path?a=b"; e != g {
+               t.Errorf("expected URL %q; got %q", e, g)
+       }
+       if g, e := req.FormValue("a"), "b"; e != g {
+               t.Errorf("expected FormValue(a) %q; got %q", e, g)
+       }
+       if req.Trailer == nil {
+               t.Errorf("unexpected nil Trailer")
+       }
+}
+
+func TestRequestWithoutHost(t *testing.T) {
+       env := map[string]string{
+               "HTTP_HOST":      "",
+               "REQUEST_METHOD": "GET",
+               "REQUEST_URI":    "/path?a=b",
+               "CONTENT_LENGTH": "123",
+       }
+       req, err := requestFromEnvironment(env)
+       if err != nil {
+               t.Fatalf("requestFromEnvironment: %v", err)
+       }
+       if g, e := req.RawURL, "/path?a=b"; e != g {
+               t.Errorf("expected RawURL %q; got %q", e, g)
+       }
+       if req.URL == nil {
+               t.Fatalf("unexpected nil URL")
+       }
+       if g, e := req.URL.String(), "/path?a=b"; e != g {
+               t.Errorf("expected URL %q; got %q", e, g)
+       }
+}
similarity index 86%
rename from src/pkg/http/cgi/cgi.go
rename to src/pkg/http/cgi/host.go
index dba59efa27c31b338e9cb3f238b9ce68b9cec14c..4a2efc781823d81e15254d99378c42ccf7daaa22 100644 (file)
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// This file implements the host side of CGI (being the webserver
+// parent process).
+
 // Package cgi implements CGI (Common Gateway Interface) as specified
 // in RFC 3875.
 //
@@ -12,6 +15,7 @@
 package cgi
 
 import (
+       "bytes"
        "encoding/line"
        "exec"
        "fmt"
@@ -19,7 +23,7 @@ import (
        "io"
        "log"
        "os"
-       "path"
+       "path/filepath"
        "regexp"
        "strconv"
        "strings"
@@ -29,10 +33,12 @@ var trailingPort = regexp.MustCompile(`:([0-9]+)$`)
 
 // Handler runs an executable in a subprocess with a CGI environment.
 type Handler struct {
-       Path   string      // path to the CGI executable
-       Root   string      // root URI prefix of handler or empty for "/"
+       Path string // path to the CGI executable
+       Root string // root URI prefix of handler or empty for "/"
+
        Env    []string    // extra environment variables to set, if any
        Logger *log.Logger // optional log for errors or nil to use log.Print
+       Args   []string    // optional arguments to pass to child process
 }
 
 func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -73,9 +79,20 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
                "SERVER_PORT=" + port,
        }
 
-       for k, _ := range req.Header {
+       if len(req.Cookie) > 0 {
+               b := new(bytes.Buffer)
+               for idx, c := range req.Cookie {
+                       if idx > 0 {
+                               b.Write([]byte("; "))
+                       }
+                       fmt.Fprintf(b, "%s=%s", c.Name, c.Value)
+               }
+               env = append(env, "HTTP_COOKIE="+b.String())
+       }
+
+       for k, v := range req.Header {
                k = strings.Map(upperCaseAndUnderscore, k)
-               env = append(env, "HTTP_"+k+"="+req.Header.Get(k))
+               env = append(env, "HTTP_"+k+"="+strings.Join(v, ", "))
        }
 
        if req.ContentLength > 0 {
@@ -89,15 +106,17 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
                env = append(env, h.Env...)
        }
 
-       // TODO: use filepath instead of path when available
-       cwd, pathBase := path.Split(h.Path)
+       cwd, pathBase := filepath.Split(h.Path)
        if cwd == "" {
                cwd = "."
        }
 
+       args := []string{h.Path}
+       args = append(args, h.Args...)
+
        cmd, err := exec.Run(
                pathBase,
-               []string{h.Path},
+               args,
                env,
                cwd,
                exec.Pipe,        // stdin
similarity index 92%
rename from src/pkg/http/cgi/cgi_test.go
rename to src/pkg/http/cgi/host_test.go
index daf9a2cb3eba1bfbc0a67552aef0c9c42b5f6b57..3362ae5805927e4fd518b9e1f515603376adaee2 100644 (file)
@@ -176,6 +176,28 @@ func TestPathInfoDirRoot(t *testing.T) {
        runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
 }
 
+func TestDupHeaders(t *testing.T) {
+       if skipTest(t) {
+               return
+       }
+       h := &Handler{
+               Path: "testdata/test.cgi",
+       }
+       expectedMap := map[string]string{
+               "env-REQUEST_URI":     "/myscript/bar?a=b",
+               "env-SCRIPT_FILENAME": "testdata/test.cgi",
+               "env-HTTP_COOKIE":     "nom=NOM; yum=YUM",
+               "env-HTTP_X_FOO":      "val1, val2",
+       }
+       runCgiTest(t, h, "GET /myscript/bar?a=b HTTP/1.0\n"+
+               "Cookie: nom=NOM\n"+
+               "Cookie: yum=YUM\n"+
+               "X-Foo: val1\n"+
+               "X-Foo: val2\n"+
+               "Host: example.com\n\n",
+               expectedMap)
+}
+
 func TestPathInfoNoRoot(t *testing.T) {
        if skipTest(t) {
                return
diff --git a/src/pkg/http/cgi/matryoshka_test.go b/src/pkg/http/cgi/matryoshka_test.go
new file mode 100644 (file)
index 0000000..4bf9c19
--- /dev/null
@@ -0,0 +1,74 @@
+// 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.
+
+// Tests a Go CGI program running under a Go CGI host process.
+// Further, the two programs are the same binary, just checking
+// their environment to figure out what mode to run in.
+
+package cgi
+
+import (
+       "fmt"
+       "http"
+       "os"
+       "testing"
+)
+
+// This test is a CGI host (testing host.go) that runs its own binary
+// as a child process testing the other half of CGI (child.go).
+func TestHostingOurselves(t *testing.T) {
+       h := &Handler{
+               Path: os.Args[0],
+               Root: "/test.go",
+               Args: []string{"-test.run=TestBeChildCGIProcess"},
+       }
+       expectedMap := map[string]string{
+               "test":                  "Hello CGI-in-CGI",
+               "param-a":               "b",
+               "param-foo":             "bar",
+               "env-GATEWAY_INTERFACE": "CGI/1.1",
+               "env-HTTP_HOST":         "example.com",
+               "env-PATH_INFO":         "",
+               "env-QUERY_STRING":      "foo=bar&a=b",
+               "env-REMOTE_ADDR":       "1.2.3.4",
+               "env-REMOTE_HOST":       "1.2.3.4",
+               "env-REQUEST_METHOD":    "GET",
+               "env-REQUEST_URI":       "/test.go?foo=bar&a=b",
+               "env-SCRIPT_FILENAME":   os.Args[0],
+               "env-SCRIPT_NAME":       "/test.go",
+               "env-SERVER_NAME":       "example.com",
+               "env-SERVER_PORT":       "80",
+               "env-SERVER_SOFTWARE":   "go",
+       }
+       replay := runCgiTest(t, h, "GET /test.go?foo=bar&a=b HTTP/1.0\nHost: example.com\n\n", expectedMap)
+
+       if expected, got := "text/html; charset=utf-8", replay.Header.Get("Content-Type"); got != expected {
+               t.Errorf("got a Content-Type of %q; expected %q", got, expected)
+       }
+       if expected, got := "X-Test-Value", replay.Header.Get("X-Test-Header"); got != expected {
+               t.Errorf("got a X-Test-Header of %q; expected %q", got, expected)
+       }
+}
+
+// Note: not actually a test.
+func TestBeChildCGIProcess(t *testing.T) {
+       if os.Getenv("REQUEST_METHOD") == "" {
+               // Not in a CGI environment; skipping test.
+               return
+       }
+       Serve(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+               rw.SetHeader("X-Test-Header", "X-Test-Value")
+               fmt.Fprintf(rw, "test=Hello CGI-in-CGI\n")
+               req.ParseForm()
+               for k, vv := range req.Form {
+                       for _, v := range vv {
+                               fmt.Fprintf(rw, "param-%s=%s\n", k, v)
+                       }
+               }
+               for _, kv := range os.Environ() {
+                       fmt.Fprintf(rw, "env-%s\n", kv)
+               }
+       }))
+       os.Exit(0)
+}
index b931b04c559202cf4f596f762c0262b78bd96f3b..8c10dde32b7263e71a23cff8d273585eb186333f 100755 (executable)
@@ -12,7 +12,7 @@ my $q = CGI->new;
 my $params = $q->Vars;
 
 my $NL = "\r\n";
-$NL = "\n" if 1 || $params->{mode} eq "NL";
+$NL = "\n" if $params->{mode} eq "NL";
 
 my $p = sub {
   print "$_[0]$NL";
index 428d9446e2af7576a9249d8072a82a224ed0bf6f..c48aa8d67db382674f54864607fe107ba66747b0 100644 (file)
@@ -6,7 +6,6 @@
 
 // TODO(rsc):
 //     logging
-//     cgi support
 //     post support
 
 package http