Repository: google/go-safeweb (go)

Issue:
Add support for Query parameters
The current implementation of `safehttp.Form` type also supports parsing query parameters as a `Form` object and returning any parsing errors that occurred. This follows the implementation of `net/http` and prevents users from calling `URL.Query()`, which silently discards parsing errors. We might want to consider separating the URL from the form type as future work.

Pull request:
Implementing safehttp.URL type and a way of accessing URL query parameters
Fixes #33

A new `safehttp.URL` type is implemented. It is very similar to the `net/url.URL` type. 

You can now access URL query parameters by using the `safehttp.URL.Query` method which returns a `safehttp.Form` containing the values.

`net/url.URL` contains a lot more functionality than `safehttp.URL`. The rest of the functionality can be implemented later as needed.

Patch:
diff --git a/docs/high-level-design.md b/docs/high-level-design.md
index 48f1413..e1a3338 100644
--- a/docs/high-level-design.md
+++ b/docs/high-level-design.md
@@ -74,7 +74,7 @@ address, following issues (not an exhaustive list):
 - [**CSP (content security policy)**](https://csp.withgoogle.com/docs/index.html) -
   e.g. by automatically adding script nonces to HTML responses, adding relevant
   security headers
-- **Transport Security** - e.g. by enforcing HSTS support
+- **Transport Security** - e.g. by [enforcing HSTS support](plugins/hsts.md)
 - **IFraming** - e.g. by setting relevant HTTP headers to restrict framing or
   providing server-side support for origin selection
 - **Auth (access control)** - e.g. by providing infrastructure for plugging in
diff --git a/docs/plugins.md b/docs/plugins.md
new file mode 100644
index 0000000..16a77d1
--- /dev/null
+++ b/docs/plugins.md
@@ -0,0 +1,10 @@
+# Plugins
+
+The framework supports plugins. They provide a flexible way to address security 
+issues by setting non-overridable security headers or interrupting and
+responding to incoming requests before they reach the handler.
+
+## Provided plugins
+
+- [HSTS](plugins/hsts.md): Automatically redirects HTTP traffic to HTTPS and
+sets the `Strict-Transport-Security` header.
diff --git a/docs/plugins/hsts.md b/docs/plugins/hsts.md
new file mode 100644
index 0000000..b1e558f
--- /dev/null
+++ b/docs/plugins/hsts.md
@@ -0,0 +1,34 @@
+_Authors: grenfeldt@google.com_
+
+# HSTS Plugin
+
+HSTS<sup>1</sup> (HTTP Strict Transport Security) informs browsers that a
+website should only be accessed using HTTPS and not HTTP. This plugin enforces
+HSTS by redirecting all HTTP traffic to HTTPS and by setting the
+`Strict-Transport-Security` header on all outgoing HTTPS traffic.
+
+1) HSTS: [RFC](https://tools.ietf.org/html/rfc6797),
+[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security),
+[Wikipedia](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+
+## Usage
+
+To construct the plugin with safe default settings, use: `hsts.NewPlugin()`.
+
+## Options
+
+**Option (Default value)**: Description.
+
+- **MaxAge (`2 years`)**: The amount of time that the browser should remember to
+only access this site using HTTPS. The default value is 2 years as recommended
+by https://hstspreload.org/.
+- **IncludeSubDomains (`enabled`)**: When this is enabled, the browser knows
+that all subdomains should also only be accessed via HTTPS.
+- **Preload (`disabled`)**: If enabled, the domain is eligible to be added as
+part of the browser HSTS preload list. This is disabled by default to prevent
+adding domains to the preload list unintentionally. For more info, see:
+https://hstspreload.org/.
+- **BehindProxy (`disabled`)**: If this server is placed behind a proxy that
+terminates HTTPS traffic then only HTTP traffic will be received by this server.
+This option disables redirection of HTTP to HTTPS and always sends the
+`Strict-Transport-Security` header, even over HTTP.
diff --git a/safehttp/incoming_request.go b/safehttp/incoming_request.go
index 41ffba3..11aa27b 100644
--- a/safehttp/incoming_request.go
+++ b/safehttp/incoming_request.go
@@ -18,7 +18,6 @@ import (
 	"crypto/tls"
 	"fmt"
 	"net/http"
-	"net/url"
 	"strings"
 	"sync"
 )
@@ -29,15 +28,17 @@ type IncomingRequest struct {
 	Header    Header
 	parseOnce sync.Once
 	TLS       *tls.ConnectionState
-	URL       *url.URL
+	URL       *URL
 }
 
-func newIncomingRequest(req *http.Request) *IncomingRequest {
+// NewIncomingRequest creates an IncomingRequest
+// from an http.Request.
+func NewIncomingRequest(req *http.Request) *IncomingRequest {
 	return &IncomingRequest{
 		req:    req,
 		Header: newHeader(req.Header),
 		TLS:    req.TLS,
-		URL:    req.URL,
+		URL:    &URL{url: req.URL},
 	}
 }
 
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 4dbea25..76ddc98 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -141,5 +141,5 @@ func (m methodHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	h.ServeHTTP(newResponseWriter(m.disp, w), newIncomingRequest(r))
+	h.ServeHTTP(NewResponseWriter(m.disp, w), NewIncomingRequest(r))
 }
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index dd2ceac..e2bfc55 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -28,7 +28,7 @@ import (
 type Plugin struct {
 	// MaxAge is the duration that the browser should remember
 	// that a site is only to be accessed using HTTPS. MaxAge
-	// must be positive.
+	// must be positive. It will be rounded to seconds before use.
 	MaxAge time.Duration
 
 	// DisableIncludeSubDomains disables the includeSubDomains directive.
diff --git a/safehttp/prototype.go b/safehttp/prototype.go
index fc22c19..5729f94 100644
--- a/safehttp/prototype.go
+++ b/safehttp/prototype.go
@@ -31,7 +31,7 @@ func NewMachinery(h HandleFunc, d Dispatcher) *Machinery {
 
 // HandleRequest TODO
 func (m *Machinery) HandleRequest(w http.ResponseWriter, req *http.Request) {
-	rw := newResponseWriter(m.d, w)
-	ir := newIncomingRequest(req)
+	rw := NewResponseWriter(m.d, w)
+	ir := NewIncomingRequest(req)
 	m.h(rw, ir)
 }
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 3cda51a..1467141 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -29,7 +29,9 @@ type ResponseWriter struct {
 	header Header
 }
 
-func newResponseWriter(d Dispatcher, rw http.ResponseWriter) ResponseWriter {
+// NewResponseWriter creates a ResponseWriter from
+// a safehttp.Dispatcher and a http.ResponseWriter.
+func NewResponseWriter(d Dispatcher, rw http.ResponseWriter) ResponseWriter {
 	header := newHeader(rw.Header())
 	return ResponseWriter{d: d, rw: rw, header: header}
 }
@@ -53,6 +55,16 @@ func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
 	return Result{}
 }
 
+// ClientError TODO
+func (w *ResponseWriter) ClientError(code StatusCode) Result {
+	if code < 400 || code >= 500 {
+		// TODO(@mihalimara22): Replace this when we decide how to handle this case
+		panic("wrong method called")
+	}
+	http.Error(w.rw, http.StatusText(int(code)), int(code))
+	return Result{}
+}
+
 // ServerError TODO
 func (w *ResponseWriter) ServerError(code StatusCode) Result {
 	if code < 500 || code >= 600 {
diff --git a/safehttp/url.go b/safehttp/url.go
new file mode 100644
index 0000000..356be06
--- /dev/null
+++ b/safehttp/url.go
@@ -0,0 +1,73 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp
+
+import "net/url"
+
+// URL represents a parsed URL (technically, a URI reference).
+type URL struct {
+	url *url.URL
+}
+
+// Query parses the query string in the URL and returns a form
+// containing its values. The returned error describes the first
+// decoding error encountered, if any.
+func (u URL) Query() (Form, error) {
+	v, err := url.ParseQuery(u.url.RawQuery)
+	if err != nil {
+		return Form{}, err
+	}
+	return Form{values: map[string][]string(v)}, nil
+}
+
+// String reassembles the URL into a valid URL string.
+//
+// The method uses the net/url.EscapedPath method to obtain the path.
+// See the net/url.EscapedPath method for more details.
+func (u URL) String() string {
+	// The escaping is perfomed by u.url.String()
+	return u.url.String()
+}
+
+// Host returns the host or the host:port of the URL.
+func (u URL) Host() string {
+	return u.url.Host
+}
+
+// Hostname returns the host of the URL, stripping any valid
+// port number if present.
+//
+// If the result is enclosed in square brackets, as literal IPv6
+// addresses are, the square brackets are removed from the result.
+func (u URL) Hostname() string {
+	return u.url.Hostname()
+}
+
+// Port returns the port part of the URL. If the
+// host doesn't contain a valid port number, Port returns an
+// empty string.
+func (u URL) Port() string {
+	return u.url.Port()
+}
+
+// Path returns the path of the URL.
+//
+// Note that the path is stored in decoded form: /%47%6f%2f
+// becomes /Go/. A consequence is that it is impossible to tell
+// which slashes in the path were slashes in the raw URL and which
+// were %2f.
+func (u URL) Path() string {
+	return u.url.Path
+}


Test patch:
diff --git a/internal/requesttesting/test_harness.go b/internal/requesttesting/test_harness.go
index cc0c93b..143020c 100644
--- a/internal/requesttesting/test_harness.go
+++ b/internal/requesttesting/test_harness.go
@@ -22,6 +22,7 @@ import (
 	"io"
 	"net"
 	"net/http"
+	"runtime"
 	"sync"
 )
 
@@ -128,6 +129,18 @@ func MakeRequest(ctx context.Context, req []byte, callback func(*http.Request))
 	if n == 4096 {
 		return nil, errors.New("response larger than or equal to 4096 bytes")
 	}
-
+	listener.clientEndpoint.Close()
+
+	// Don't remove the line below! This line is to stop server.Shutdown from
+	// getting stuck polling for idle connections to close. When we close the
+	// connection on the line above, it takes a little while for the other
+	// goroutines to shut everything down. When server.Shutdown is called it
+	// checks if there are any idle, non-closed connections left and waits for
+	// them to close. It performs this check every 0.5 s. But our connection
+	// above takes a lot less time than 0.5 s to close. Therefore, if we just
+	// give up execution for a moment to let the other goroutines close
+	// everything before calling server.Shutdown, we don't get punished by
+	// having to wait 0.5s every time we try to shut down.
+	runtime.Gosched()
 	return resp[:n], server.Shutdown(ctx)
 }
diff --git a/safehttp/plugins/hsts/hsts_test.go b/safehttp/plugins/hsts/hsts_test.go
index eb1e846..0c62afd 100644
--- a/safehttp/plugins/hsts/hsts_test.go
+++ b/safehttp/plugins/hsts/hsts_test.go
@@ -15,68 +15,20 @@
 package hsts_test
 
 import (
-	"html/template"
-	"io"
-	"net/http"
-	"net/http/httptest"
-	"strings"
 	"testing"
 	"time"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-safeweb/safehttp"
 	"github.com/google/go-safeweb/safehttp/plugins/hsts"
-	"github.com/google/safehtml"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
 )
 
-type testDispatcher struct{}
-
-func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
-	switch x := resp.(type) {
-	case safehtml.HTML:
-		_, err := rw.Write([]byte(x.String()))
-		return err
-	default:
-		panic("not a safe response type")
-	}
-}
-
-func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
-	switch x := t.(type) {
-	case *template.Template:
-		return x.Execute(rw, data)
-	default:
-		panic("not a safe response type")
-	}
-}
-
-type responseRecorder struct {
-	header http.Header
-	writer io.Writer
-	status int
-}
-
-func newResponseRecorder(w io.Writer) *responseRecorder {
-	return &responseRecorder{header: http.Header{}, writer: w, status: http.StatusOK}
-}
-
-func (r *responseRecorder) Header() http.Header {
-	return r.header
-}
-
-func (r *responseRecorder) WriteHeader(statusCode int) {
-	r.status = statusCode
-}
-
-func (r *responseRecorder) Write(data []byte) (int, error) {
-	return r.writer.Write(data)
-}
-
 func TestHSTS(t *testing.T) {
 	var test = []struct {
 		name        string
 		plugin      hsts.Plugin
-		req         *http.Request
+		req         *safehttp.IncomingRequest
 		wantStatus  int
 		wantHeaders map[string][]string
 		wantBody    string
@@ -84,7 +36,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "HTTPS",
 			plugin:     hsts.NewPlugin(),
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				"Strict-Transport-Security": {"max-age=63072000; includeSubDomains"}, // 63072000 seconds is two years
@@ -94,7 +46,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "HTTP",
 			plugin:     hsts.NewPlugin(),
-			req:        httptest.NewRequest("GET", "http://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "http://localhost/", nil),
 			wantStatus: 301,
 			wantHeaders: map[string][]string{
 				"Content-Type": {"text/html; charset=utf-8"},
@@ -105,7 +57,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "HTTP behind proxy",
 			plugin:     hsts.Plugin{BehindProxy: true},
-			req:        httptest.NewRequest("GET", "http://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "http://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
@@ -116,7 +68,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "Preload",
 			plugin:     hsts.Plugin{Preload: true, DisableIncludeSubDomains: true},
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
@@ -127,7 +79,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "Preload and IncludeSubDomains",
 			plugin:     hsts.Plugin{Preload: true},
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
@@ -138,7 +90,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "No preload and no includeSubDomains",
 			plugin:     hsts.Plugin{DisableIncludeSubDomains: true},
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				// max-age=0 tells the browser to expire the HSTS protection.
@@ -149,7 +101,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "Custom maxage",
 			plugin:     hsts.Plugin{MaxAge: 3600 * time.Second},
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 200,
 			wantHeaders: map[string][]string{
 				"Strict-Transport-Security": {"max-age=3600; includeSubDomains"}, // 3600 seconds is 1 hour
@@ -159,7 +111,7 @@ func TestHSTS(t *testing.T) {
 		{
 			name:       "Negative maxage",
 			plugin:     hsts.Plugin{MaxAge: -1 * time.Second},
-			req:        httptest.NewRequest("GET", "https://localhost/", nil),
+			req:        safehttptest.NewRequest("GET", "https://localhost/", nil),
 			wantStatus: 500,
 			wantHeaders: map[string][]string{
 				"Content-Type":           {"text/plain; charset=utf-8"},
@@ -171,22 +123,19 @@ func TestHSTS(t *testing.T) {
 
 	for _, tt := range test {
 		t.Run(tt.name, func(t *testing.T) {
-			m := safehttp.NewMachinery(tt.plugin.Before, &testDispatcher{})
-
-			b := &strings.Builder{}
-			rr := newResponseRecorder(b)
+			rr := safehttptest.NewResponseRecorder()
 
-			m.HandleRequest(rr, tt.req)
+			tt.plugin.Before(rr.ResponseWriter, tt.req)
 
-			if rr.status != tt.wantStatus {
-				t.Errorf("status code got: %v want: %v", rr.status, tt.wantStatus)
+			if rr.Status() != tt.wantStatus {
+				t.Errorf("status code got: %v want: %v", rr.Status(), tt.wantStatus)
 			}
 
 			if diff := cmp.Diff(tt.wantHeaders, map[string][]string(rr.Header())); diff != "" {
 				t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
 			}
 
-			if got := b.String(); got != tt.wantBody {
+			if got := rr.Body(); got != tt.wantBody {
 				t.Errorf("response body got: %q want: %q", got, tt.wantBody)
 			}
 		})
@@ -195,20 +144,16 @@ func TestHSTS(t *testing.T) {
 
 func TestStrictTransportSecurityAlreadyImmutable(t *testing.T) {
 	p := hsts.NewPlugin()
-	m := safehttp.NewMachinery(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-		w.Header().Claim("Strict-Transport-Security")
-		return p.Before(w, r)
-	}, &testDispatcher{})
 
-	req := httptest.NewRequest("GET", "https://localhost/", nil)
+	req := safehttptest.NewRequest("GET", "https://localhost/", nil)
 
-	b := &strings.Builder{}
-	rr := newResponseRecorder(b)
+	rr := safehttptest.NewResponseRecorder()
+	rr.ResponseWriter.Header().Claim("Strict-Transport-Security")
 
-	m.HandleRequest(rr, req)
+	p.Before(rr.ResponseWriter, req)
 
-	if want := 500; rr.status != want {
-		t.Errorf("status code got: %v want: %v", rr.status, want)
+	if want := 500; rr.Status() != want {
+		t.Errorf("status code got: %v want: %v", rr.Status(), want)
 	}
 
 	wantHeaders := map[string][]string{
@@ -219,7 +164,7 @@ func TestStrictTransportSecurityAlreadyImmutable(t *testing.T) {
 		t.Errorf("rr.Header() mismatch (-want +got):\n%s", diff)
 	}
 
-	if got, want := b.String(), "Internal Server Error\n"; got != want {
+	if got, want := rr.Body(), "Internal Server Error\n"; got != want {
 		t.Errorf("response body got: %q want: %q", got, want)
 	}
 }
diff --git a/safehttp/safehttptest/recorder.go b/safehttp/safehttptest/recorder.go
new file mode 100644
index 0000000..5222848
--- /dev/null
+++ b/safehttp/safehttptest/recorder.go
@@ -0,0 +1,121 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttptest
+
+import (
+	"html/template"
+	"io"
+	"net/http"
+	"strings"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/safehtml"
+)
+
+type testDispatcher struct{}
+
+func (testDispatcher) Write(rw http.ResponseWriter, resp safehttp.Response) error {
+	switch x := resp.(type) {
+	case safehtml.HTML:
+		_, err := rw.Write([]byte(x.String()))
+		return err
+	default:
+		panic("not a safe response type")
+	}
+}
+
+func (testDispatcher) ExecuteTemplate(rw http.ResponseWriter, t safehttp.Template, data interface{}) error {
+	switch x := t.(type) {
+	case *template.Template:
+		return x.Execute(rw, data)
+	default:
+		panic("not a safe response type")
+	}
+}
+
+// ResponseRecorder encapsulates a safehttp.ResponseWriter that records
+// mutations for later inspection in tests. The safehttp.ResponseWriter
+// should be passed as part of the handler function in tests.
+type ResponseRecorder struct {
+	safehttp.ResponseWriter
+	rw *responseWriter
+	b  *strings.Builder
+}
+
+// NewResponseRecorder creates a ResponseRecorder from the default testDispatcher.
+func NewResponseRecorder() *ResponseRecorder {
+	var b strings.Builder
+	rw := newResponseWriter(&b)
+	return &ResponseRecorder{
+		rw:             rw,
+		b:              &b,
+		ResponseWriter: safehttp.NewResponseWriter(testDispatcher{}, rw),
+	}
+}
+
+// NewResponseRecorderFromDispatcher creates a ResponseRecorder from a
+// provided safehttp.Dispatcher.
+func NewResponseRecorderFromDispatcher(d safehttp.Dispatcher) *ResponseRecorder {
+	var b strings.Builder
+	rw := newResponseWriter(&b)
+	return &ResponseRecorder{
+		rw:             rw,
+		b:              &b,
+		ResponseWriter: safehttp.NewResponseWriter(d, rw),
+	}
+}
+
+// Header returns the recorded response headers.
+func (r *ResponseRecorder) Header() http.Header {
+	return r.rw.Header()
+}
+
+// Status returns the recorded response status code.
+func (r *ResponseRecorder) Status() int {
+	return r.rw.status
+}
+
+// Body returns the recorded response body.
+func (r *ResponseRecorder) Body() string {
+	return r.b.String()
+}
+
+// responseWriter is an implementation of the http.ResponseWriter interface used
+// for constructing an HTTP response.
+type responseWriter struct {
+	header http.Header
+	writer io.Writer
+	status int
+}
+
+func newResponseWriter(w io.Writer) *responseWriter {
+	return &responseWriter{
+		header: http.Header{},
+		writer: w,
+		status: http.StatusOK,
+	}
+}
+
+func (r *responseWriter) Header() http.Header {
+	return r.header
+}
+
+func (r *responseWriter) WriteHeader(statusCode int) {
+	r.status = statusCode
+}
+
+func (r *responseWriter) Write(data []byte) (int, error) {
+	return r.writer.Write(data)
+}
diff --git a/safehttp/safehttptest/request.go b/safehttp/safehttptest/request.go
new file mode 100644
index 0000000..b2e494a
--- /dev/null
+++ b/safehttp/safehttptest/request.go
@@ -0,0 +1,52 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttptest
+
+import (
+	"io"
+	"net/http/httptest"
+
+	"github.com/google/go-safeweb/safehttp"
+)
+
+// NewRequest returns a new incoming server Request,
+// suitable for passing to an http.Handler for testing.
+//
+// The target is the RFC 7230 "request-target": it may
+// be either a path or an absolute URL. If target is an
+// absolute URL, the host name from the URL is used.
+// Otherwise, "example.com" is used.
+//
+// The TLS field is set to a non-nil dummy value if
+// target has scheme "https".
+//
+// The Request.Proto is always HTTP/1.1.
+//
+// An empty method means "GET".
+//
+// The provided body may be nil. If the body is of type
+// *bytes.Reader, *strings.Reader, or *bytes.Buffer, the
+// Request.ContentLength is set.
+//
+// NewRequest panics on error for ease of use in testing,
+// where a panic is acceptable.
+//
+// To generate a client HTTP request instead of a server
+// request, see the NewRequest function in the net/http
+// package.
+func NewRequest(method, target string, body io.Reader) *safehttp.IncomingRequest {
+	req := httptest.NewRequest(method, target, body)
+	return safehttp.NewIncomingRequest(req)
+}
diff --git a/safehttp/url_test.go b/safehttp/url_test.go
new file mode 100644
index 0000000..fc4a97b
--- /dev/null
+++ b/safehttp/url_test.go
@@ -0,0 +1,205 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package safehttp
+
+import (
+	"net/url"
+	"testing"
+)
+
+func TestURLToString(t *testing.T) {
+	want := "http://www.example.com/asdf?fruit=apple"
+
+	netURL, err := url.Parse(want)
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?fruit=apple") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if got := u.String(); got != want {
+		t.Errorf("u.String() got: %v want: %v", got, want)
+	}
+}
+
+func TestURLHost(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "www.example.com:1337",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Host(); got != tt.want {
+				t.Errorf("u.Host() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLHostname(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "www.example.com",
+		},
+		{
+			name: "Ipv6 and port",
+			url:  "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000/asdf?fruit=apple",
+			want: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Hostname(); got != tt.want {
+				t.Errorf("u.Hostname() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLPort(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Just host",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "",
+		},
+		{
+			name: "Host and port",
+			url:  "http://www.example.com:1337/asdf?fruit=apple",
+			want: "1337",
+		},
+		{
+			name: "HTTPS",
+			url:  "https://www.example.com/asdf?fruit=apple",
+			want: "",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			if got := u.Port(); got != tt.want {
+				t.Errorf("u.Port() got: %v want: %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLPath(t *testing.T) {
+	netURL, err := url.Parse("http://www.example.com/asdf?fruit=apple")
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?fruit=apple") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if got, want := u.Path(), "/asdf"; got != want {
+		t.Errorf("u.Path() got: %v want: %v", got, want)
+	}
+}
+
+func TestURLQuery(t *testing.T) {
+	var test = []struct {
+		name string
+		url  string
+		want string
+	}{
+		{
+			name: "Normal",
+			url:  "http://www.example.com/asdf?fruit=apple",
+			want: "apple",
+		},
+		{
+			name: "Empty",
+			url:  "http://www.example.com/asdf",
+			want: "",
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(tt.name, func(t *testing.T) {
+			netURL, err := url.Parse(tt.url)
+			if err != nil {
+				t.Fatalf("url.Parse(tt.url) got: %v want: nil", err)
+			}
+
+			u := URL{url: netURL}
+			f, err := u.Query()
+			if err != nil {
+				t.Errorf("u.Query() got: %v want: nil", err)
+			}
+
+			if got := f.String("fruit", ""); got != tt.want {
+				t.Errorf(`f.String("fruit", "") got: %q want: %q`, got, tt.want)
+			}
+		})
+	}
+}
+
+func TestURLInvalidQuery(t *testing.T) {
+	netURL, err := url.Parse("http://www.example.com/asdf?%xx=abc")
+	if err != nil {
+		t.Fatalf(`url.Parse("http://www.example.com/asdf?%%xx=abc") got: %v want: nil`, err)
+	}
+
+	u := URL{url: netURL}
+	if _, err = u.Query(); err == nil {
+		t.Error("u.Query() got: nil want: error")
+	}
+}