]> Cypherpunks repositories - gostls13.git/commitdiff
net/http, net/url: permit Request-URI "*"
authorBrad Fitzpatrick <bradfitz@golang.org>
Tue, 11 Dec 2012 17:07:27 +0000 (12:07 -0500)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 11 Dec 2012 17:07:27 +0000 (12:07 -0500)
Also, implement a global OPTIONS * handler, like Apache.

Permit sending "*" requests to handlers, but not path-based
(ServeMux) handlers.  That means people can go out of their
way to support SSDP or SIP or whatever, but most users will be
unaffected.

See RFC 2616 Section 5.1.2 (Request-URI)
See RFC 2616 Section 9.2 (OPTIONS)

Fixes #3692

R=rsc
CC=golang-dev
https://golang.org/cl/6868095

src/pkg/net/http/readrequest_test.go
src/pkg/net/http/serve_test.go
src/pkg/net/http/server.go
src/pkg/net/url/url.go
src/pkg/net/url/url_test.go

index 2e03c658aa90bdb6ada675e8970c61e558aaf185..ffdd6a892dab7f2f1265c87ea983fff293b53176 100644 (file)
@@ -247,6 +247,54 @@ var reqTests = []reqTest{
                noTrailer,
                noError,
        },
+
+       // SSDP Notify request. golang.org/issue/3692
+       {
+               "NOTIFY * HTTP/1.1\r\nServer: foo\r\n\r\n",
+               &Request{
+                       Method: "NOTIFY",
+                       URL: &url.URL{
+                               Path: "*",
+                       },
+                       Proto:      "HTTP/1.1",
+                       ProtoMajor: 1,
+                       ProtoMinor: 1,
+                       Header: Header{
+                               "Server": []string{"foo"},
+                       },
+                       Close:         false,
+                       ContentLength: 0,
+                       RequestURI:    "*",
+               },
+
+               noBody,
+               noTrailer,
+               noError,
+       },
+
+       // OPTIONS request. Similar to golang.org/issue/3692
+       {
+               "OPTIONS * HTTP/1.1\r\nServer: foo\r\n\r\n",
+               &Request{
+                       Method: "OPTIONS",
+                       URL: &url.URL{
+                               Path: "*",
+                       },
+                       Proto:      "HTTP/1.1",
+                       ProtoMajor: 1,
+                       ProtoMinor: 1,
+                       Header: Header{
+                               "Server": []string{"foo"},
+                       },
+                       Close:         false,
+                       ContentLength: 0,
+                       RequestURI:    "*",
+               },
+
+               noBody,
+               noTrailer,
+               noError,
+       },
 }
 
 func TestReadRequest(t *testing.T) {
index 8ca227f9dec71cc7e233c1218690638c3f945134..7167101273246b89aed10c9523753cdf0129c586 100644 (file)
@@ -1288,6 +1288,58 @@ For:
        ts.Close()
 }
 
+func TestOptions(t *testing.T) {
+       uric := make(chan string, 2) // only expect 1, but leave space for 2
+       mux := NewServeMux()
+       mux.HandleFunc("/", func(w ResponseWriter, r *Request) {
+               uric <- r.RequestURI
+       })
+       ts := httptest.NewServer(mux)
+       defer ts.Close()
+
+       conn, err := net.Dial("tcp", ts.Listener.Addr().String())
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer conn.Close()
+
+       // An OPTIONS * request should succeed.
+       _, err = conn.Write([]byte("OPTIONS * HTTP/1.1\r\nHost: foo.com\r\n\r\n"))
+       if err != nil {
+               t.Fatal(err)
+       }
+       br := bufio.NewReader(conn)
+       res, err := ReadResponse(br, &Request{Method: "OPTIONS"})
+       if err != nil {
+               t.Fatal(err)
+       }
+       if res.StatusCode != 200 {
+               t.Errorf("Got non-200 response to OPTIONS *: %#v", res)
+       }
+
+       // A GET * request on a ServeMux should fail.
+       _, err = conn.Write([]byte("GET * HTTP/1.1\r\nHost: foo.com\r\n\r\n"))
+       if err != nil {
+               t.Fatal(err)
+       }
+       res, err = ReadResponse(br, &Request{Method: "GET"})
+       if err != nil {
+               t.Fatal(err)
+       }
+       if res.StatusCode != 400 {
+               t.Errorf("Got non-400 response to GET *: %#v", res)
+       }
+
+       res, err = Get(ts.URL + "/second")
+       if err != nil {
+               t.Fatal(err)
+       }
+       res.Body.Close()
+       if got := <-uric; got != "/second" {
+               t.Errorf("Handler saw request for %q; want /second", got)
+       }
+}
+
 // goTimeout runs f, failing t if f takes more than ns to complete.
 func goTimeout(t *testing.T, d time.Duration, f func()) {
        ch := make(chan bool, 2)
index c4ddbec54f1ce86d806f4dcc64d683e96884c9ac..8cd7b11205a6be4ea44ad3f9981c3475213b43f2 100644 (file)
@@ -770,6 +770,9 @@ func (c *conn) serve() {
                if handler == nil {
                        handler = DefaultServeMux
                }
+               if req.RequestURI == "*" && req.Method == "OPTIONS" {
+                       handler = globalOptionsHandler{}
+               }
 
                // HTTP cannot have multiple simultaneous active requests.[*]
                // Until the server replies to this request, it can't read another,
@@ -1085,6 +1088,11 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
 // ServeHTTP dispatches the request to the handler whose
 // pattern most closely matches the request URL.
 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
+       if r.RequestURI == "*" {
+               w.Header().Set("Connection", "close")
+               w.WriteHeader(StatusBadRequest)
+               return
+       }
        h, _ := mux.Handler(r)
        h.ServeHTTP(w, r)
 }
@@ -1408,6 +1416,22 @@ func (tw *timeoutWriter) WriteHeader(code int) {
        tw.w.WriteHeader(code)
 }
 
+// globalOptionsHandler responds to "OPTIONS *" requests.
+type globalOptionsHandler struct{}
+
+func (globalOptionsHandler) ServeHTTP(w ResponseWriter, r *Request) {
+       w.Header().Set("Content-Length", "0")
+       if r.ContentLength != 0 {
+               // Read up to 4KB of OPTIONS body (as mentioned in the
+               // spec as being reserved for future use), but anything
+               // over that is considered a waste of server resources
+               // (or an attack) and we abort and close the connection,
+               // courtesy of MaxBytesReader's EOF behavior.
+               mb := MaxBytesReader(w, r.Body, 4<<10)
+               io.Copy(ioutil.Discard, mb)
+       }
+}
+
 // loggingConn is used for debugging.
 type loggingConn struct {
        name string
index 82db0367bc682eda736bc065323f22f10518fce1..71758fe49e00997be3458ea2c0dfc20b704f379b 100644 (file)
@@ -361,6 +361,11 @@ func parse(rawurl string, viaRequest bool) (url *URL, err error) {
        }
        url = new(URL)
 
+       if rawurl == "*" {
+               url.Path = "*"
+               return
+       }
+
        // Split off possible leading "http:", "mailto:", etc.
        // Cannot contain escaped characters.
        if url.Scheme, rest, err = getscheme(rawurl); err != nil {
index 4a0918940383abf0a8f9b41301db313da341bdf8..4d3545dadb74f7a3e11945a99472fb7ffbfa66ba 100644 (file)
@@ -277,7 +277,7 @@ func TestParse(t *testing.T) {
 
 const pathThatLooksSchemeRelative = "//not.a.user@not.a.host/just/a/path"
 
-var parseRequestUrlTests = []struct {
+var parseRequestURLTests = []struct {
        url           string
        expectedValid bool
 }{
@@ -289,10 +289,11 @@ var parseRequestUrlTests = []struct {
        {"//not.a.user@%66%6f%6f.com/just/a/path/also", true},
        {"foo.html", false},
        {"../dir/", false},
+       {"*", true},
 }
 
 func TestParseRequestURI(t *testing.T) {
-       for _, test := range parseRequestUrlTests {
+       for _, test := range parseRequestURLTests {
                _, err := ParseRequestURI(test.url)
                valid := err == nil
                if valid != test.expectedValid {