profile
viewpoint
Damian Bogel kele @Google Zurich, Switzerland http://kele.codes Security by design, security by default @Google. Doing some Go stuff there too.

google/go-safeweb 338

Secure-by-default HTTP servers in Go.

kele/collectd 3

(Windows port) The system statistics collection daemon. Mirror of the canonical repository (updated hourly), but feel free to send Pull Requests!

kele/foxy 3

Proxy server for helping with focus written in Rust ...

kele/.dotfiles 1

dotfiles

empijei/report-to-parser 0

A simple package to parse reports coming from browsers implementing the Reporting API

kele/.scripts 0

Collection of handy scrips

kele/coffeepp 0

Little language that compiles into C++

kele/cpp_tricks 0

C++ tricks

Pull request review commentgoogle/go-safeweb

Interceptors' `Commit` and `OnError` methods can no longer affect the flow by calling write operations

 type ResponseWriter interface { 	// 	// If the ResponseWriter has already been written to, then this method panics. 	Redirect(r *IncomingRequest, url string, code StatusCode) Result+} +// ResponseHeadersWriter is used to alter the HTTP response headers.+//+// A ResponseHeadersWriter may not be used after the Handler.ServeHTTP method has returned.+type ResponseHeadersWriter interface {

I would document that if somethings has already been written this entire type methods set becomes a no-op.

kele

comment created time in 10 hours

startedvdumoulin/conv_arithmetic

started time in 20 hours

fork mattiasgrenfeldt/taitan

Git-powered content management for Bawang

fork in a day

fork mattiasgrenfeldt/knock-knock

Vem där? Kolla vem som är på SM (i framtiden även in och utlista)

fork in 2 days

fork mattiasgrenfeldt/yoggi

Yoggi, som i yoghurt, som i fil, som i statiska filer. Bra jobbat Mauritz

fork in 2 days

fork mattiasgrenfeldt/verksamt

System for keeping track of the computer science chapters operational plan (verksamhetsplan)

fork in 2 days

fork mattiasgrenfeldt/Meta-TV

Hacky application for displaying news and stuff running on the monitors where we eat lunch.

fork in 2 days

fork mattiasgrenfeldt/dfunkt

Hantering av Datasektionens nuvarande och tidigare funktionärer

fork in 2 days

fork mattiasgrenfeldt/cashflow

Django and React project to manage receipts and reimbursements at Datasektionen

https://cashflow.datasektionen.se

fork in 2 days

Pull request review commentgoogle/go-safeweb

Consolidated request handling and the ResponseWriter implementation to one object

+// 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 (+	"fmt"+	"net/http"+)++// TODO(kele): come up with a better name than this.

Maybe we can call this a "flight"

kele

comment created time in 3 days

Pull request review commentgoogle/go-safeweb

Consolidated request handling and the ResponseWriter implementation to one object

+// 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 (+	"fmt"+	"net/http"+)++// TODO(kele): come up with a better name than this.+type task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	// TODO(kele): we need to distinguish between calling Write and actually+	// writing to the net/http.ResponseWriter.+	written      bool+	writtenError bool+}++// DeprecatedNewResponseWriter creates a ResponseWriter implementation that has+// been historically used for testing. DO NOT USE this function, it's going+// to be removed soon.+//+// The main problem with this is the implementation of the returned+// ResponseWriter is incomplete, i.e. lacks the Handler and Interceptors fields+// of the HandlerConfig needed by the task. For the existing tests that use it,+// it's fine, but they should be migrated.+//+// TODO(kele): remove this once we have a better option to provide a+// ResponseWriter implementation for interceptor testing.+func DeprecatedNewResponseWriter(rw http.ResponseWriter, dispatcher Dispatcher) ResponseWriter {+	return &task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++// HandlerConfig is the safe HTTP handler configuration, including the+// dispatcher and interceptors.+type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func processRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {

personal taste: I'd make processRequest a method of HandlerConfig.

kele

comment created time in 3 days

Pull request review commentgoogle/go-safeweb

Consolidated request handling and the ResponseWriter implementation to one object

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")+	}+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	http.Redirect(t.rw, r.req, url, int(code))+	return Result{}+}++// Header returns the collection of headers that will be set+// on the response. Headers must be set before writing a+// response (e.g. Write, WriteTemplate).+func (t *Task) Header() Header {+	return t.header+}++// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.+// The provided cookie must have a valid Name, otherwise an error will be+// returned.+func (t *Task) SetCookie(c *Cookie) error {+	return t.header.addCookie(c)+}++// SetCode allows setting a response status. If the response was already+// written, trying to set the status code will have no effect. This method will+// panic if an invalid status code is passed (i.e. not in the range 1XX-5XX).+//+// If SetCode was called before NoContent, Redirect or WriteError, the status+// code set by the latter will be the actual response status.+//+// TODO(empijei@, kele@, maramihali@): decide what should be done if the+// code passed is either 3XX (redirect) or 4XX-5XX (client/server error).+func (t *Task) SetCode(code StatusCode) {+	if t.written {+		return+	}+	if code < 100 || code >= 600 {+		panic("invalid status code")+	}+	t.code = code+}++// commitPhase calls the Commit phases of all the interceptors. This stage will+// run before a response is written to the ResponseWriter. If a response is+// written to the ResponseWriter in a Commit phase then the Commit phases of the+// remaining interceptors won't execute.+//+// TODO: BIG WARNING, if ResponseWriter.Write and ResponseWriter.WriteTemplate+// are called in Commit then this will recurse. CommitResponseWriter was an+// attempt to prevent this by not giving access to Write and WriteTemplate in+// the Commit phase.+func (t *Task) commitPhase(resp Response) {+	for i := len(t.cfg.Interceptors) - 1; i >= 0; i-- {+		t.cfg.Interceptors[i].Commit(t, t.req, resp)+	}+}++// errorPhase calls the OnError phases of all the interceptors associated with+// a handler. This stage runs before an error response is written to the+// ResponseWriter.+//+// TODO: BIG WARNING, if an interceptor attempts to write to the ResponseWriter+// in the OnError phase will result in an irrecoverable error.+func (t *Task) errorPhase(resp Response) {+	for i := len(t.cfg.Interceptors) - 1; i >= 0; i-- {+		t.cfg.Interceptors[i].OnError(t, t.req, resp)

sometimes we call it after the response has been partially written, there isn't much it can modify at that point, is there?

kele

comment created time in 3 days

Pull request review commentgoogle/go-safeweb

Consolidated request handling and the ResponseWriter implementation to one object

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}

+1 on Skip and on asking the Dispatcher to handle a NoContent type.

kele

comment created time in 3 days

PublicEvent

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")+	}+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	http.Redirect(t.rw, r.req, url, int(code))+	return Result{}+}++// Header returns the collection of headers that will be set+// on the response. Headers must be set before writing a+// response (e.g. Write, WriteTemplate).+func (t *Task) Header() Header {+	return t.header+}++// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.+// The provided cookie must have a valid Name, otherwise an error will be+// returned.+func (t *Task) SetCookie(c *Cookie) error {+	return t.header.addCookie(c)+}++// SetCode allows setting a response status. If the response was already+// written, trying to set the status code will have no effect. This method will+// panic if an invalid status code is passed (i.e. not in the range 1XX-5XX).+//+// If SetCode was called before NoContent, Redirect or WriteError, the status+// code set by the latter will be the actual response status.+//+// TODO(empijei@, kele@, maramihali@): decide what should be done if the+// code passed is either 3XX (redirect) or 4XX-5XX (client/server error).+func (t *Task) SetCode(code StatusCode) {+	if t.written {+		return+	}+	if code < 100 || code >= 600 {+		panic("invalid status code")+	}+	t.code = code+}++// commitPhase calls the Commit phases of all the interceptors. This stage will+// run before a response is written to the ResponseWriter. If a response is+// written to the ResponseWriter in a Commit phase then the Commit phases of the+// remaining interceptors won't execute.+//+// TODO: BIG WARNING, if ResponseWriter.Write and ResponseWriter.WriteTemplate+// are called in Commit then this will recurse. CommitResponseWriter was an+// attempt to prevent this by not giving access to Write and WriteTemplate in+// the Commit phase.+func (t *Task) commitPhase(resp Response) {+	for i := len(t.cfg.Interceptors) - 1; i >= 0; i-- {+		t.cfg.Interceptors[i].Commit(t, t.req, resp)+	}+}++// errorPhase calls the OnError phases of all the interceptors associated with+// a handler. This stage runs before an error response is written to the+// ResponseWriter.+//+// TODO: BIG WARNING, if an interceptor attempts to write to the ResponseWriter+// in the OnError phase will result in an irrecoverable error.+func (t *Task) errorPhase(resp Response) {+	for i := len(t.cfg.Interceptors) - 1; i >= 0; i-- {+		t.cfg.Interceptors[i].OnError(t, t.req, resp)

Should we maybe pass a smaller interface to OnError that doesn't have Write* methods so we make sure Write is not called in this step?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")+	}+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	http.Redirect(t.rw, r.req, url, int(code))+	return Result{}+}++// Header returns the collection of headers that will be set+// on the response. Headers must be set before writing a+// response (e.g. Write, WriteTemplate).+func (t *Task) Header() Header {+	return t.header+}++// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.+// The provided cookie must have a valid Name, otherwise an error will be+// returned.+func (t *Task) SetCookie(c *Cookie) error {+	return t.header.addCookie(c)+}++// SetCode allows setting a response status. If the response was already+// written, trying to set the status code will have no effect. This method will+// panic if an invalid status code is passed (i.e. not in the range 1XX-5XX).+//+// If SetCode was called before NoContent, Redirect or WriteError, the status+// code set by the latter will be the actual response status.+//+// TODO(empijei@, kele@, maramihali@): decide what should be done if the+// code passed is either 3XX (redirect) or 4XX-5XX (client/server error).+func (t *Task) SetCode(code StatusCode) {+	if t.written {+		return+	}+	if code < 100 || code >= 600 {+		panic("invalid status code")+	}+	t.code = code+}++// commitPhase calls the Commit phases of all the interceptors. This stage will+// run before a response is written to the ResponseWriter. If a response is+// written to the ResponseWriter in a Commit phase then the Commit phases of the+// remaining interceptors won't execute.+//+// TODO: BIG WARNING, if ResponseWriter.Write and ResponseWriter.WriteTemplate+// are called in Commit then this will recurse. CommitResponseWriter was an+// attempt to prevent this by not giving access to Write and WriteTemplate in+// the Commit phase.
kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")+	}+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	http.Redirect(t.rw, r.req, url, int(code))+	return Result{}+}++// Header returns the collection of headers that will be set+// on the response. Headers must be set before writing a+// response (e.g. Write, WriteTemplate).
// response.
kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")
		panic(fmt.Sprintf("wrong method called: redirect with status %d", code))
kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true+	t.writtenError = true+	resp := &ErrorResponse{Code: code}+	t.errorPhase(resp)+	http.Error(t.rw, http.StatusText(int(resp.Code)), int(resp.Code))+	return Result{}+}++// Redirect responds with a redirect to the given url, using code as the status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) Redirect(r *IncomingRequest, url string, code StatusCode) Result {+	if code < 300 || code >= 400 {+		panic("wrong method called")+	}+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	http.Redirect(t.rw, r.req, url, int(code))+	return Result{}+}++// Header returns the collection of headers that will be set+// on the response. Headers must be set before writing a+// response (e.g. Write, WriteTemplate).+func (t *Task) Header() Header {+	return t.header+}++// SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.+// The provided cookie must have a valid Name, otherwise an error will be+// returned.+func (t *Task) SetCookie(c *Cookie) error {

Maybe we should change this to AddCookie?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {

Does this need to be exported?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool

We are not distinguishing between actual writes and write calls. This means that when we start writing an error we don't know if the response serialization was already started (and thus writing the error is useless).

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}++// WriteError writes an error response (400-599) according to the provided+// status code.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) WriteError(code StatusCode) Result {+	// TODO: accept custom error responses that need to go through the dispatcher.+	if t.writtenError {+		panic("ResponseWriter.WriteError called twice")+	}+	t.written = true

See comment I left on the Task type: at this point we should know if we already started writing or not.

Depending on the answer we might have to skip the http.Error call on line 139.

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {+	rw  http.ResponseWriter+	req *IncomingRequest++	cfg HandlerConfig++	code   StatusCode+	header Header++	written      bool+	writtenError bool+}++// NewTask creates a ResponseWriter from a safehttp.Dispatcher, an+// http.ResponseWriter and a reference to the current IncomingRequest being served.+// The IncomingRequest will only be used by the commit phase, which only runs when+// the ServeMux is used, and can be passed as nil in tests.+// TODO: remove the IncomingRequest parameter.++// ProperNewTask TODO+func BrokenNewTask(rw http.ResponseWriter, dispatcher Dispatcher) *Task {+	return &Task{+		cfg:    HandlerConfig{Dispatcher: dispatcher},+		rw:     rw,+		header: newHeader(rw.Header()),+	}+}++type HandlerConfig struct {+	Handler      Handler+	Dispatcher   Dispatcher+	Interceptors []ConfiguredInterceptor+}++func ProcessRequest(cfg HandlerConfig, rw http.ResponseWriter, req *http.Request) {+	t := &Task{+		cfg:    cfg,+		rw:     rw,+		header: newHeader(rw.Header()),+		req:    NewIncomingRequest(req),+	}++	// The `net/http` package recovers handler panics, but we cannot rely on that behavior here.+	// The reason is, we might need to run After/Commit stages of the interceptors before we+	// respond with a 500 Internal Server Error.+	defer func() {+		if r := recover(); r != nil {+			t.WriteError(StatusInternalServerError)+		}+	}()++	for _, it := range t.cfg.Interceptors {+		it.Before(t, t.req)+		if t.written {+			return+		}+	}++	t.cfg.Handler.ServeHTTP(t, t.req)+	if !t.written {+		t.NoContent()+	}+}++// Write dispatches the response to the Dispatcher. This will be written to the+// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.+//+// TODO: replace panics with proper error handling when writing the response fails.+func (t *Task) Write(resp Response) Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(resp)++	ct, err := t.cfg.Dispatcher.ContentType(resp)+	if err != nil {+		panic(err)+	}+	t.rw.Header().Set("Content-Type", ct)++	if t.code == 0 {+		t.code = StatusOK+	}+	t.rw.WriteHeader(int(t.code))++	if err := t.cfg.Dispatcher.Write(t.rw, resp); err != nil {+		panic(err)+	}+	return Result{}+}++// NoContent responds with a 204 No Content response.+//+// If the ResponseWriter has already been written to, then this method will panic.+func (t *Task) NoContent() Result {+	if t.written {+		panic("ResponseWriter was already written to")+	}+	t.written = true+	t.commitPhase(NoContentResponse{})+	t.rw.WriteHeader(int(StatusNoContent))+	return Result{}+}

Question for another PR: should we maybe make NoContent a type to pass to Write instead?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

 func (s *ServeMuxConfig) Intercept(i Interceptor) {  // Mux returns the ServeMux with a copy of the current configuration. func (s *ServeMuxConfig) Mux() *ServeMux {-	m := newServeMux(s.dispatcher)-	for _, it := range s.interceptors {-		m.install(it)+	dispatcher := s.dispatcher+	if dispatcher == nil {+		dispatcher = DefaultDispatcher{} 	}+	// pattern -> method -> HandlerConfig+	handlers := map[string]map[string]HandlerConfig{} 	for _, hr := range s.handlers {-		m.handle(hr.pattern, hr.method, hr.handler, hr.cfgs...)+		if handlers[hr.pattern] == nil {+			handlers[hr.pattern] = map[string]HandlerConfig{}+		}+		if _, ok := handlers[hr.pattern][hr.method]; ok {+			panic(fmt.Sprintf("double registration of (pattern = %q, method = %q)", hr.pattern, hr.method))+		}+		handlers[hr.pattern][hr.method] =+			HandlerConfig{+				Dispatcher:   dispatcher,+				Handler:      hr.handler,+				Interceptors: configureInterceptors(s.interceptors, hr.cfgs),+			} 	}-	return m+	m := http.NewServeMux()+	registerHandlers(m, handlers)+	return &ServeMux{mux: m}+}++func configureInterceptors(interceptors []Interceptor, cfgs []InterceptorConfig) []ConfiguredInterceptor {+	var its []ConfiguredInterceptor+	for _, it := range interceptors {+		var cfg InterceptorConfig+		for _, c := range cfgs {+			if c.Match(it) {

Shouldn't we validate there is at most one cfg per interceptor?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

+// 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/http"+)++type Task struct {

I am not sure I like the "Task" name but I kinda like that we wrapped an HTTP roundtrip into a single thing/action.

I think we should still name it along the lines of ResponseWriter or users will get confused/alienated at first.

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

 func (s *ServeMuxConfig) Intercept(i Interceptor) {  // Mux returns the ServeMux with a copy of the current configuration. func (s *ServeMuxConfig) Mux() *ServeMux {-	m := newServeMux(s.dispatcher)-	for _, it := range s.interceptors {-		m.install(it)+	dispatcher := s.dispatcher+	if dispatcher == nil {+		dispatcher = DefaultDispatcher{} 	}+	// pattern -> method -> HandlerConfig+	handlers := map[string]map[string]HandlerConfig{} 	for _, hr := range s.handlers {-		m.handle(hr.pattern, hr.method, hr.handler, hr.cfgs...)+		if handlers[hr.pattern] == nil {+			handlers[hr.pattern] = map[string]HandlerConfig{}+		}+		if _, ok := handlers[hr.pattern][hr.method]; ok {+			panic(fmt.Sprintf("double registration of (pattern = %q, method = %q)", hr.pattern, hr.method))

Can't we check this at registration time?

kele

comment created time in 17 days

Pull request review commentgoogle/go-safeweb

ResponseWriter refactoring

 const ( // Multiple handlers can be registered for a single pattern, as long as they // handle different HTTP methods. type ServeMux struct {-	mux  *http.ServeMux-	disp Dispatcher--	// Maps patterns to handlers supporting multiple HTTP methods.-	handlers  map[string]standardHandler-	interceps []Interceptor-}--// newServeMux allocates and returns a new ServeMux. If the provided dispatcher-// is nil, the DefaultDispatcher is used.-func newServeMux(d Dispatcher) *ServeMux {-	if d == nil {-		d = DefaultDispatcher{}-	}-	return &ServeMux{-		mux:      http.NewServeMux(),-		disp:     d,-		handlers: map[string]standardHandler{},-	}-}--func (m *ServeMux) handle(pattern string, method string, h Handler, cfgs ...InterceptorConfig) {-	var interceps []ConfiguredInterceptor-	for _, it := range m.interceps {-		var cfg InterceptorConfig-		for _, c := range cfgs {-			if c.Match(it) {-				cfg = c-				break+	mux *http.ServeMux+}++func registerHandlers(mux *http.ServeMux, handlers map[string]map[string]HandlerConfig) {+	for pattern, handlersPerMethod := range handlers {+		pattern := pattern

This is not needed.

kele

comment created time in 17 days

more