github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/examples/sample-application/server/server.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build go1.16 16 // +build go1.16 17 18 // Package server implements the application server. 19 // 20 // This is the piece that a product team owns and implements. They use APIs from 21 // go-safeweb/examples/sample-application/secure and Go Safe 22 // Web to configure the HTTP handlers. 23 // 24 // If strictly necessary, the product team might use restricted APIs using 25 // security-reviewed exemptions (e.g. with go-safeweb/cmd/banchek). 26 package server 27 28 import ( 29 "github.com/google/go-safeweb/safehttp" 30 31 "embed" 32 33 "github.com/google/go-safeweb/safehttp/plugins/htmlinject" 34 "github.com/google/safehtml/template" 35 36 "github.com/google/go-safeweb/examples/sample-application/secure/auth" 37 "github.com/google/go-safeweb/examples/sample-application/secure/responses" 38 "github.com/google/go-safeweb/examples/sample-application/storage" 39 ) 40 41 //go:embed static 42 var staticFiles embed.FS 43 44 //go:embed templates 45 var templatesFS embed.FS 46 47 var templates *template.Template 48 49 func init() { 50 tplSrc := template.TrustedSourceFromConstant("templates/*.tpl.html") 51 var err error 52 // htmlinject automatically injects CSP nonces and XSRF tokens placeholders. 53 templates, err = htmlinject.LoadGlobEmbed(nil, htmlinject.LoadConfig{}, tplSrc, templatesFS) 54 if err != nil { 55 panic(err) 56 } 57 } 58 59 type serverDeps struct { 60 db *storage.DB 61 } 62 63 func Load(db *storage.DB, mux *safehttp.ServeMux) { 64 deps := &serverDeps{ 65 db: db, 66 } 67 68 // Private endpoints, only accessible to authenticated users (default). 69 mux.Handle("/notes/", "GET", getNotesHandler(deps)) 70 mux.Handle("/notes", "POST", postNotesHandler(deps)) 71 mux.Handle("/logout", "POST", logoutHandler(deps)) 72 73 // Public enpoints, no auth checks performed. 74 mux.Handle("/login", "POST", postLoginHandler(deps), auth.Skip{}) 75 mux.Handle("/static/", "GET", safehttp.FileServerEmbed(staticFiles), auth.Skip{}) 76 mux.Handle("/", "GET", indexHandler(deps), auth.Skip{}) 77 } 78 79 func getNotesHandler(deps *serverDeps) safehttp.Handler { 80 return safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 81 user := auth.User(r) 82 notes := deps.db.GetNotes(user) 83 return safehttp.ExecuteNamedTemplate(rw, templates, "notes.tpl.html", map[string]interface{}{ 84 "notes": notes, 85 "user": user, 86 }) 87 }) 88 } 89 90 func postNotesHandler(deps *serverDeps) safehttp.Handler { 91 noFormErr := responses.NewError( 92 safehttp.StatusBadRequest, 93 template.MustParseAndExecuteToHTML(`Please submit a valid form with "title" and "text" parameters.`), 94 ) 95 noFieldsErr := responses.NewError( 96 safehttp.StatusBadRequest, 97 template.MustParseAndExecuteToHTML("Both title and text must be specified."), 98 ) 99 100 return safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 101 form, err := r.PostForm() 102 if err != nil { 103 return rw.WriteError(noFormErr) 104 } 105 title := form.String("title", "") 106 body := form.String("text", "") 107 if title == "" || body == "" { 108 return rw.WriteError(noFieldsErr) 109 } 110 user := auth.User(r) 111 deps.db.AddOrEditNote(user, storage.Note{Title: title, Text: body}) 112 113 notes := deps.db.GetNotes(user) 114 return safehttp.ExecuteNamedTemplate(rw, templates, "notes.tpl.html", map[string]interface{}{ 115 "notes": notes, 116 "user": user, 117 }) 118 }) 119 } 120 121 func indexHandler(deps *serverDeps) safehttp.Handler { 122 return safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 123 user := auth.User(r) 124 if user != "" { 125 return safehttp.Redirect(rw, r, "/notes/", safehttp.StatusTemporaryRedirect) 126 } 127 return safehttp.ExecuteNamedTemplate(rw, templates, "index.tpl.html", nil) 128 }) 129 } 130 131 // Logout and Login handlers would normally be centralized and provided by a separate package owned by the security team. 132 // Since this is a simple example application they are here together with the rest. 133 func logoutHandler(deps *serverDeps) safehttp.Handler { 134 return safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 135 auth.ClearSession(r) 136 return safehttp.Redirect(rw, r, "/", safehttp.StatusSeeOther) 137 }) 138 } 139 140 func postLoginHandler(deps *serverDeps) safehttp.Handler { 141 invalidAuthErr := responses.NewError( 142 safehttp.StatusBadRequest, 143 template.MustParseAndExecuteToHTML("Please specify a username and a password, both must be non-empty and your password must match the one you use to register."), 144 ) 145 146 // Always return the same error to not leak the existence of a user. 147 return safehttp.HandlerFunc(func(rw safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result { 148 form, err := r.PostForm() 149 if err != nil { 150 return rw.WriteError(invalidAuthErr) 151 } 152 username := form.String("username", "") 153 password := form.String("password", "") 154 if username == "" || password == "" { 155 return rw.WriteError(invalidAuthErr) 156 } 157 if err := deps.db.AddOrAuthUser(username, password); err != nil { 158 return rw.WriteError(invalidAuthErr) 159 } 160 auth.CreateSession(r, username) 161 return safehttp.Redirect(rw, r, "/notes/", safehttp.StatusSeeOther) 162 }) 163 }