From d64a18a27e13550606063ca970a5a004611d6881 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 8 Mar 2011 08:01:19 -0800 Subject: [PATCH] cgi: child support (e.g. Go CGI under Apache) 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 | 3 +- src/pkg/http/cgi/child.go | 202 ++++++++++++++++++ src/pkg/http/cgi/child_test.go | 83 +++++++ src/pkg/http/cgi/{cgi.go => host.go} | 35 ++- .../http/cgi/{cgi_test.go => host_test.go} | 22 ++ src/pkg/http/cgi/matryoshka_test.go | 74 +++++++ src/pkg/http/cgi/testdata/test.cgi | 2 +- src/pkg/http/server.go | 1 - 8 files changed, 411 insertions(+), 11 deletions(-) create mode 100644 src/pkg/http/cgi/child.go create mode 100644 src/pkg/http/cgi/child_test.go rename src/pkg/http/cgi/{cgi.go => host.go} (86%) rename src/pkg/http/cgi/{cgi_test.go => host_test.go} (92%) create mode 100644 src/pkg/http/cgi/matryoshka_test.go diff --git a/src/pkg/http/cgi/Makefile b/src/pkg/http/cgi/Makefile index 02f6cfc9e7..19b1039c26 100644 --- a/src/pkg/http/cgi/Makefile +++ b/src/pkg/http/cgi/Makefile @@ -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 index 0000000000..50f90e5263 --- /dev/null +++ b/src/pkg/http/cgi/child.go @@ -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 index 0000000000..db0e09cf66 --- /dev/null +++ b/src/pkg/http/cgi/child_test.go @@ -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) + } +} diff --git a/src/pkg/http/cgi/cgi.go b/src/pkg/http/cgi/host.go similarity index 86% rename from src/pkg/http/cgi/cgi.go rename to src/pkg/http/cgi/host.go index dba59efa27..4a2efc7818 100644 --- a/src/pkg/http/cgi/cgi.go +++ b/src/pkg/http/cgi/host.go @@ -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 diff --git a/src/pkg/http/cgi/cgi_test.go b/src/pkg/http/cgi/host_test.go similarity index 92% rename from src/pkg/http/cgi/cgi_test.go rename to src/pkg/http/cgi/host_test.go index daf9a2cb3e..3362ae5805 100644 --- a/src/pkg/http/cgi/cgi_test.go +++ b/src/pkg/http/cgi/host_test.go @@ -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 index 0000000000..4bf9c19cb7 --- /dev/null +++ b/src/pkg/http/cgi/matryoshka_test.go @@ -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) +} diff --git a/src/pkg/http/cgi/testdata/test.cgi b/src/pkg/http/cgi/testdata/test.cgi index b931b04c55..8c10dde32b 100755 --- a/src/pkg/http/cgi/testdata/test.cgi +++ b/src/pkg/http/cgi/testdata/test.cgi @@ -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"; diff --git a/src/pkg/http/server.go b/src/pkg/http/server.go index 428d9446e2..c48aa8d67d 100644 --- a/src/pkg/http/server.go +++ b/src/pkg/http/server.go @@ -6,7 +6,6 @@ // TODO(rsc): // logging -// cgi support // post support package http