1
0
mirror of https://github.com/golang/go synced 2024-11-24 18:10:02 -07:00

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
This commit is contained in:
Brad Fitzpatrick 2011-03-08 08:01:19 -08:00
parent 8c76218f89
commit d64a18a27e
8 changed files with 411 additions and 11 deletions

View File

@ -6,6 +6,7 @@ include ../../../Make.inc
TARG=http/cgi
GOFILES=\
cgi.go\
child.go\
host.go\
include ../../../Make.pkg

202
src/pkg/http/cgi/child.go Normal file
View File

@ -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
}

View File

@ -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)
}
}

View 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

View 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

View File

@ -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)
}

View File

@ -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";

View File

@ -6,7 +6,6 @@
// TODO(rsc):
// logging
// cgi support
// post support
package http