1
0
mirror of https://github.com/golang/go synced 2024-11-23 08:10:03 -07:00

net/http/cgi: kill child CGI process on copy error

Fixes #7196

LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews, iant
https://golang.org/cl/69970052
This commit is contained in:
Brad Fitzpatrick 2014-03-06 11:24:28 -08:00
parent a766277742
commit 5db255fa3c
2 changed files with 104 additions and 1 deletions

View File

@ -214,6 +214,9 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
internalError(err)
return
}
if hook := testHookStartProcess; hook != nil {
hook(cmd.Process)
}
defer cmd.Wait()
defer stdoutRead.Close()
@ -292,6 +295,13 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
_, err = io.Copy(rw, linebody)
if err != nil {
h.printf("cgi: copy error: %v", err)
// And kill the child CGI process so we don't hang on
// the deferred cmd.Wait above if the error was just
// the client (rw) going away. If it was a read error
// (because the child died itself), then the extra
// kill of an already-dead process is harmless (the PID
// won't be reused until the Wait above).
cmd.Process.Kill()
}
}
@ -348,3 +358,5 @@ func upperCaseAndUnderscore(r rune) rune {
// TODO: other transformations in spec or practice?
return r
}
var testHookStartProcess func(*os.Process) // nil except for some tests

View File

@ -9,10 +9,15 @@
package cgi
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
)
// This test is a CGI host (testing host.go) that runs its own binary
@ -51,7 +56,78 @@ func TestHostingOurselves(t *testing.T) {
}
}
// Test that a child handler only writing headers works.
type customWriterRecorder struct {
w io.Writer
*httptest.ResponseRecorder
}
func (r *customWriterRecorder) Write(p []byte) (n int, err error) {
return r.w.Write(p)
}
type limitWriter struct {
w io.Writer
n int
}
func (w *limitWriter) Write(p []byte) (n int, err error) {
if len(p) > w.n {
p = p[:w.n]
}
if len(p) > 0 {
n, err = w.w.Write(p)
w.n -= n
}
if w.n == 0 {
err = errors.New("past write limit")
}
return
}
// If there's an error copying the child's output to the parent, test
// that we kill the child.
func TestKillChildAfterCopyError(t *testing.T) {
defer func() { testHookStartProcess = nil }()
proc := make(chan *os.Process, 1)
testHookStartProcess = func(p *os.Process) {
proc <- p
}
h := &Handler{
Path: os.Args[0],
Root: "/test.go",
Args: []string{"-test.run=TestBeChildCGIProcess"},
}
req, _ := http.NewRequest("GET", "http://example.com/test.cgi?write-forever=1", nil)
rec := httptest.NewRecorder()
var out bytes.Buffer
const writeLen = 50 << 10
rw := &customWriterRecorder{&limitWriter{&out, writeLen}, rec}
donec := make(chan bool, 1)
go func() {
h.ServeHTTP(rw, req)
donec <- true
}()
select {
case <-donec:
if out.Len() != writeLen || out.Bytes()[0] != 'a' {
t.Errorf("unexpected output: %q", out.Bytes())
}
case <-time.After(5 * time.Second):
t.Errorf("timeout. ServeHTTP hung and didn't kill the child process?")
select {
case p := <-proc:
p.Kill()
t.Logf("killed process")
default:
t.Logf("didn't kill process")
}
}
}
// Test that a child handler writing only headers works.
func TestChildOnlyHeaders(t *testing.T) {
h := &Handler{
Path: os.Args[0],
@ -67,6 +143,15 @@ func TestChildOnlyHeaders(t *testing.T) {
}
}
type neverEnding byte
func (b neverEnding) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = byte(b)
}
return len(p), nil
}
// Note: not actually a test.
func TestBeChildCGIProcess(t *testing.T) {
if os.Getenv("REQUEST_METHOD") == "" {
@ -79,6 +164,12 @@ func TestBeChildCGIProcess(t *testing.T) {
if req.FormValue("no-body") == "1" {
return
}
if req.FormValue("write-forever") == "1" {
io.Copy(rw, neverEnding('a'))
for {
time.Sleep(5 * time.Second) // hang forever, until killed
}
}
fmt.Fprintf(rw, "test=Hello CGI-in-CGI\n")
for k, vv := range req.Form {
for _, v := range vv {