Repository: google/go-safeweb (go)

Issue:
Provide a way to do local development (plugins should be informed about this to change behavior)
Currently, the `xsrf` plugin sets a safe cookie with the `Secure` option, that disallows sending them over HTTP(no S). This is problematic in local development,

Pull request description:
Add a way to set up the framework in local development mode.
This PR also wires the HSTS plugin and Cookie behavior to disable HTTPS-only behaviors if running in local development mode.

Fixes #171

Patch:
diff --git a/safehttp/cookie.go b/safehttp/cookie.go
index 9114161..b289633 100644
--- a/safehttp/cookie.go
+++ b/safehttp/cookie.go
@@ -28,7 +28,7 @@ type Cookie struct {
 
 // NewCookie creates a new Cookie with safe default settings.
 // Those safe defaults are:
-//  - Secure: true
+//  - Secure: true (if the framework is not in dev mode)
 //  - HttpOnly: true
 //  - SameSite: Lax
 // For more info about all the options, see:
@@ -38,7 +38,7 @@ func NewCookie(name, value string) *Cookie {
 		&http.Cookie{
 			Name:     name,
 			Value:    value,
-			Secure:   true,
+			Secure:   !isLocalDev,
 			HttpOnly: true,
 			SameSite: http.SameSiteLaxMode,
 		},
diff --git a/safehttp/dev.go b/safehttp/dev.go
new file mode 100644
index 0000000..5ebe135
--- /dev/null
+++ b/safehttp/dev.go
@@ -0,0 +1,41 @@
+// 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
+
+var (
+	isLocalDev bool
+	// freezeLocalDev is set on Mux construction.
+	freezeLocalDev bool
+)
+
+// UseLocalDev instructs the framework to disable some security mechanisms that
+// would make local development hard or impossible. This cannot be undone without
+// restarting the program and should only be done before any other function or type
+// of the framework is used.
+// This function should ideally be called by the main package immediately after
+// flag parsing.
+// This configuration is not valid for production use.
+func UseLocalDev() {
+	if freezeLocalDev {
+		panic("UseLocalDev can only be called before any other part of the framework")
+	}
+	isLocalDev = true
+}
+
+// IsLocalDev returns whether the framework is set up to use local development
+// rules. Please see the doc on UseLocalDev.
+func IsLocalDev() bool {
+	return isLocalDev
+}
diff --git a/safehttp/mux.go b/safehttp/mux.go
index 5dd1ae6..3974338 100644
--- a/safehttp/mux.go
+++ b/safehttp/mux.go
@@ -16,6 +16,7 @@ package safehttp
 
 import (
 	"fmt"
+	"log"
 	"net/http"
 )
 
@@ -148,6 +149,11 @@ func (s *ServeMuxConfig) Intercept(i Interceptor) {
 
 // Mux returns the ServeMux with a copy of the current configuration.
 func (s *ServeMuxConfig) Mux() *ServeMux {
+	freezeLocalDev = true
+	if IsLocalDev() {
+		log.Println("Warning: creating safehttp.Mux in dev mode. This configuration is not valid for production use")
+	}
+
 	dispatcher := s.Dispatcher
 	if dispatcher == nil {
 		dispatcher = DefaultDispatcher{}
diff --git a/safehttp/plugins/hsts/hsts.go b/safehttp/plugins/hsts/hsts.go
index 4056f14..679a449 100644
--- a/safehttp/plugins/hsts/hsts.go
+++ b/safehttp/plugins/hsts/hsts.go
@@ -17,7 +17,8 @@
 // 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 HTTPS responses.
+// Strict-Transport-Security header on all HTTPS responses. Please note that this
+// only applies if the framework is not run in dev mode.
 //
 // More info:
 //  - MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
@@ -85,6 +86,10 @@ func Default() Interceptor {
 // is received the Strict-Transport-Security header is applied to the
 // response.
 func (it Interceptor) Before(w safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
+	if safehttp.IsLocalDev() {
+		return safehttp.NotWritten()
+	}
+
 	if it.MaxAge < 0 {
 		return w.WriteError(safehttp.StatusInternalServerError)
 	}


Test patch:
diff --git a/safehttp/defaults/defaults_test.go b/safehttp/defaults/defaults_test.go
index c63e732..5e33267 100644
--- a/safehttp/defaults/defaults_test.go
+++ b/safehttp/defaults/defaults_test.go
@@ -25,7 +25,7 @@ import (
 )
 
 func TestServeMuxConfig(t *testing.T) {
-	t.Run("can load", func(t *testing.T) {
+	t.Run("can load in prod mode", func(t *testing.T) {
 		const resp = "response"
 		cfg, _ := ServeMuxConfig([]string{"test.host.example"}, "test-xsrf-key")
 		cfg.Handle("/test", "GET", safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
diff --git a/tests/integration/devmode/devmode_test.go b/tests/integration/devmode/devmode_test.go
new file mode 100644
index 0000000..48fa277
--- /dev/null
+++ b/tests/integration/devmode/devmode_test.go
@@ -0,0 +1,72 @@
+// 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 devmode_test
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/defaults"
+	"github.com/google/safehtml"
+)
+
+func TestDevMode(t *testing.T) {
+	t.Run("can load in dev mode", func(t *testing.T) {
+		safehttp.UseLocalDev()
+		if !safehttp.IsLocalDev() {
+			t.Errorf("IsLocalDev(): got false, want true")
+		}
+		const resp = "response"
+		cfg, _ := defaults.ServeMuxConfig([]string{"test.host.example"}, "test-xsrf-key")
+		cfg.Handle("/test", "GET", safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			form, err := r.URL.Query()
+			if err != nil {
+				t.Errorf("Cannot parse GET form: %v", err)
+			}
+			b := form.Bool("test", false)
+			if !b {
+				t.Error("test parameter, got false, want true")
+			}
+			w.SetCookie(safehttp.NewCookie("test", "insecure"))
+			return w.Write(safehtml.HTMLEscaped(resp))
+		}))
+		mux := cfg.Mux()
+		w := httptest.NewRecorder()
+		r := httptest.NewRequest("GET", "https://test.host.example/test?test=true", nil)
+		mux.ServeHTTP(w, r)
+		if w.Result().StatusCode != 200 {
+			t.Errorf("Status: got %d, want 200", w.Result().StatusCode)
+		}
+		got, err := ioutil.ReadAll(w.Result().Body)
+		if err != nil {
+			t.Errorf("Read body: got %v", err)
+		}
+		if bytes.Compare(got, []byte(resp)) != 0 {
+			t.Errorf("body: got %q, want %q", got, resp)
+		}
+		cs := w.Result().Cookies()
+		if len(cs) == 0 {
+			t.Errorf("got no cookies, wanted cookies")
+		}
+		for _, c := range cs {
+			if c.Secure {
+				t.Errorf("got secure cookie %q, should have not been secure", c.Raw)
+			}
+		}
+	})
+}
diff --git a/tests/integration/devmodefreeze/devmodefreeze_test.go b/tests/integration/devmodefreeze/devmodefreeze_test.go
new file mode 100644
index 0000000..467bf15
--- /dev/null
+++ b/tests/integration/devmodefreeze/devmodefreeze_test.go
@@ -0,0 +1,74 @@
+// 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 devmode_test
+
+import (
+	"bytes"
+	"io/ioutil"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/defaults"
+	"github.com/google/safehtml"
+)
+
+func TestDevMode(t *testing.T) {
+	t.Run("can load in prod mode and can't change afterwards", func(t *testing.T) {
+		const resp = "response"
+		cfg, _ := defaults.ServeMuxConfig([]string{"test.host.example"}, "test-xsrf-key")
+		cfg.Handle("/test", "GET", safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
+			form, err := r.URL.Query()
+			if err != nil {
+				t.Errorf("Cannot parse GET form: %v", err)
+			}
+			b := form.Bool("test", false)
+			if !b {
+				t.Error("test parameter, got false, want true")
+			}
+			w.SetCookie(safehttp.NewCookie("test", "insecure"))
+			return w.Write(safehtml.HTMLEscaped(resp))
+		}))
+		mux := cfg.Mux()
+		w := httptest.NewRecorder()
+		r := httptest.NewRequest("GET", "https://test.host.example/test?test=true", nil)
+		mux.ServeHTTP(w, r)
+		if w.Result().StatusCode != 200 {
+			t.Errorf("Status: got %d, want 200", w.Result().StatusCode)
+		}
+		got, err := ioutil.ReadAll(w.Result().Body)
+		if err != nil {
+			t.Errorf("Read body: got %v", err)
+		}
+		if bytes.Compare(got, []byte(resp)) != 0 {
+			t.Errorf("body: got %q, want %q", got, resp)
+		}
+		cs := w.Result().Cookies()
+		if len(cs) == 0 {
+			t.Errorf("got no cookies, wanted cookies")
+		}
+		for _, c := range cs {
+			if !c.Secure {
+				t.Errorf("got non-secure cookie %q, should have been secure", c.Raw)
+			}
+		}
+		defer func() {
+			if r := recover(); r == nil {
+				t.Errorf("got no panic, wanted panic due to setting dev mode after running the framework")
+			}
+		}()
+		safehttp.UseLocalDev()
+	})
+}