diff --git a/build.pl b/build.pl index b596bc43f..6b0d1e690 100755 --- a/build.pl +++ b/build.pl @@ -174,7 +174,7 @@ __DATA__ - lib/go/ext/openpgp/error - lib/go/ext/openpgp/armor ./website/Makefile - # (no deps, yet) + - lib/go/line ./clients/go/camput/Makefile - lib/go/client - lib/go/blobref diff --git a/website/Makefile b/website/Makefile index fad166a5b..255bde279 100644 --- a/website/Makefile +++ b/website/Makefile @@ -1,8 +1,11 @@ include $(GOROOT)/src/Make.inc +PREREQ=$(QUOTED_GOROOT)/pkg/$(GOOS)_$(GOARCH)/camli/encoding/line.a + TARG=camweb GOFILES=\ camweb.go\ + cgi.go\ include $(GOROOT)/src/Make.cmd diff --git a/website/camweb.go b/website/camweb.go index 0e6e47e71..3173a6e8e 100644 --- a/website/camweb.go +++ b/website/camweb.go @@ -161,6 +161,7 @@ func main() { mux := http.DefaultServeMux mux.Handle("/favicon.ico", http.FileServer(path.Join(*root, "static"), "/")) mux.Handle("/static/", http.FileServer(path.Join(*root, "static"), "/static/")) + mux.Handle("/test.cgi", &CgiHandler{ExecutablePath: path.Join(*root, "test.cgi")}) mux.HandleFunc("/", mainHandler) if err := http.ListenAndServe(*httpAddr, mux); err != nil { diff --git a/website/cgi.go b/website/cgi.go new file mode 100644 index 000000000..77bef8404 --- /dev/null +++ b/website/cgi.go @@ -0,0 +1,135 @@ +package main + +// Package cgi implements a CGI (Common Gateway Interface) HTTP handler as specified +// in RFC 3875. Using CGI isn't usually incredibly efficient and this package +// is intended primarily for compatibility. + +import ( + "camli/encoding/line" + "exec" + "fmt" + "http" + "io" + "log" + "os" + "strconv" + "strings" +) + +type CgiHandler struct { + ExecutablePath string + // TODO: custom log.Logger +} + +func (h *CgiHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + env := []string{ + "SERVER_SOFTWARE=golang", + "SERVER_NAME=" + req.Host, + "GATEWAY_INTERFACE=CGI/1.1", + "REQUEST_METHOD=" + req.Method, + "QUERY_STRING=" + req.URL.RawQuery, + } + + if req.ContentLength > 0 { + env = append(env, fmt.Sprintf("CONTENT_LENGTH=%d", req.ContentLength)) + } + if ctype, ok := req.Header["Content-Type"]; ok { + env = append(env, "CONTENT_TYPE=" + ctype) + } + + // TODO: let this be cleared instead at start. + env = append(env, os.Environ()...) + + cwd := h.ExecutablePath + if slash := strings.LastIndex(cwd, "/"); slash != -1 { + cwd = cwd[0:slash] + } + if !strings.HasPrefix(cwd, "/") { + cwd = "." + } + + cmd, err := exec.Run( + h.ExecutablePath, + []string{h.ExecutablePath}, + env, + cwd, + exec.Pipe, // stdin + exec.Pipe, // stdout + exec.PassThrough, // stderr (for now) + ) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + log.Printf("CGI error: %v", err) + return + } + defer func() { + // TODO: close subprocess somehow? No kill?! + cmd.Stdin.Close() + cmd.Stdout.Close() + }() + + if req.ContentLength != 0 { + go func() { + io.Copy(cmd.Stdin, req.Body) + }() + } + + linebody := line.NewReader(cmd.Stdout, 1024) + unsent := make(map[string]string) + sentStatus := false + for { + line, isPrefix, err := linebody.ReadLine() + if isPrefix { + rw.WriteHeader(http.StatusInternalServerError) + log.Printf("CGI: long header line from subprocess.") + return + } + if err == os.EOF { + break + } + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + log.Printf("CGI: error reading headers: %v", err) + return + } + if len(line) == 0 { + break + } + parts := strings.Split(string(line), ":", 2) + if len(parts) < 2 { + log.Printf("CGI: bogus header line: %s", string(line)) + continue + } + h, v := parts[0], parts[1] + h = strings.TrimSpace(h) + v = strings.TrimSpace(h) + switch { + case h == "Status": + if len(v) < 3 { + log.Printf("CGI: bogus status (short)") + return + } + code, err := strconv.Atoi(v[0:3]) + if err != nil { + log.Printf("CGI: bogus status") + return + } + rw.WriteHeader(code) + sentStatus = true + case sentStatus: + rw.SetHeader(h, v) + case !sentStatus: + unsent[h] = v + } + } + if !sentStatus { + rw.WriteHeader(http.StatusOK) + } + for h, v := range unsent { + rw.SetHeader(h, v) + } + _, err = io.Copy(rw, linebody) + if err != nil { + log.Printf("CGI: copy error: %v", err) + } +} diff --git a/website/test.cgi b/website/test.cgi new file mode 100755 index 000000000..6cf3a4171 --- /dev/null +++ b/website/test.cgi @@ -0,0 +1,30 @@ +#!/usr/bin/perl +# + +use strict; +print "Content-Type: text/html\n\n"; + +print "
\n"; + print "Stdin= [$in]\n"; + print "\n"; +} elsif ($ENV{'REQUEST_METHOD'} eq "POST") { + my $in; + sysread(STDIN, $in, $ENV{'CONTENT_LENGTH'}); + print "
\n"; + print "Stdin= [$in]\n"; + print "\n"; +} + +print "
\n"; +foreach my $key (sort(keys(%ENV))){ + print "$key", " "x(23-length($key)), "= $ENV{$key}\n"; +} + +print "\n"; + +print "\n"; +exit 0;