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:
parent
8c76218f89
commit
d64a18a27e
@ -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
202
src/pkg/http/cgi/child.go
Normal 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
|
||||
}
|
83
src/pkg/http/cgi/child_test.go
Normal file
83
src/pkg/http/cgi/child_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
74
src/pkg/http/cgi/matryoshka_test.go
Normal file
74
src/pkg/http/cgi/matryoshka_test.go
Normal 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)
|
||||
}
|
2
src/pkg/http/cgi/testdata/test.cgi
vendored
2
src/pkg/http/cgi/testdata/test.cgi
vendored
@ -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";
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
// TODO(rsc):
|
||||
// logging
|
||||
// cgi support
|
||||
// post support
|
||||
|
||||
package http
|
||||
|
Loading…
Reference in New Issue
Block a user