github.com/blend/go-sdk@v1.20220411.3/web/ctx.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package web 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/json" 14 "encoding/xml" 15 "io" 16 "net/http" 17 "net/url" 18 "time" 19 20 "github.com/blend/go-sdk/ex" 21 "github.com/blend/go-sdk/logger" 22 "github.com/blend/go-sdk/reflectutil" 23 ) 24 25 var ( 26 _ io.Closer = (*Ctx)(nil) 27 ) 28 29 // NewCtx returns a new ctx. 30 func NewCtx(w ResponseWriter, r *http.Request, options ...CtxOption) *Ctx { 31 ctx := Ctx{ 32 Response: w, 33 Request: r, 34 State: new(SyncState), 35 } 36 for _, option := range options { 37 option(&ctx) 38 } 39 return &ctx 40 } 41 42 // Ctx is the struct that represents the context for an hc request. 43 type Ctx struct { 44 // App is a reference back to the parent application. 45 App *App 46 // Auth is a reference to the app default auth manager, but 47 // can be overwritten by middleware. 48 Auth AuthManager 49 // DefaultProvider is the app default result provider by default 50 // but can be overwritten by middleware. 51 DefaultProvider ResultProvider 52 // Views is the app view cache by default but can be 53 // overwritten by middleware. 54 Views *ViewCache 55 // Response is the response writer for the request. 56 Response ResponseWriter 57 // Request is the inbound request metadata. 58 Request *http.Request 59 // Body is a cached copy of the post body of a request. 60 // It is typically set by calling `.PostBody()` on this context. 61 // If you're expecting a large post body, do not use 62 // the `.PostBody()` function, instead read directly from `.Request.Body` with 63 // a stream reader or similar. 64 Body []byte 65 // Form is a cache of parsed url form values from the post body. 66 Form url.Values 67 // State is a mutable bag of state, it contains by default 68 // state set on the application. 69 State State 70 // Session is the current auth session 71 Session *Session 72 // Route is the matching route for the request if relevant. 73 Route *Route 74 // RouteParams is a cache of parameters or variables 75 // within the route and their values. 76 RouteParams RouteParameters 77 // Log is the request specific logger. 78 Log logger.Log 79 // Tracer is the app tracer by default if one is set. 80 // It can be overwritten by middleware. 81 Tracer Tracer 82 // RequestStarted is the time the request was received. 83 RequestStarted time.Time 84 } 85 86 // Close closes the context. 87 func (rc *Ctx) Close() error { 88 if rc.Response != nil { 89 if err := rc.Response.Close(); err != nil { 90 return err 91 } 92 } 93 return nil 94 } 95 96 // WithContext sets the background context for the request. 97 func (rc *Ctx) WithContext(ctx context.Context) *Ctx { 98 *rc.Request = *rc.Request.WithContext(ctx) 99 return rc 100 } 101 102 // Context returns the context. 103 func (rc *Ctx) Context() context.Context { 104 ctx := logger.WithLabels(rc.Request.Context(), logger.GetLabels(rc.Request.Context())) 105 ctx = logger.WithLabels(ctx, rc.Labels()) 106 ctx = logger.WithAnnotations(ctx, logger.CombineAnnotations(logger.GetAnnotations(rc.Request.Context()), rc.Annotations())) 107 return ctx 108 } 109 110 // WithStateValue sets the state for a key to an object. 111 func (rc *Ctx) WithStateValue(key string, value interface{}) *Ctx { 112 rc.State.Set(key, value) 113 return rc 114 } 115 116 // StateValue returns an object in the state cache. 117 func (rc *Ctx) StateValue(key string) interface{} { 118 return rc.State.Get(key) 119 } 120 121 // Param returns a parameter from the request. 122 /* 123 It checks, in order: 124 - RouteParam 125 - QueryValue 126 - HeaderValue 127 - FormValue 128 - CookieValue 129 130 It should only be used in cases where you don't necessarily know where the param 131 value will be coming from. Where possible, use the more tightly scoped 132 param getters. 133 134 It returns the value, and a validation error if the value is not found in 135 any of the possible sources. 136 137 You can use one of the Value functions to also cast the resulting string 138 into a useful type: 139 140 typed, err := web.IntValue(rc.Param("fooID")) 141 142 */ 143 func (rc *Ctx) Param(name string) (value string, err error) { 144 if rc.RouteParams != nil { 145 value = rc.RouteParams.Get(name) 146 if value != "" { 147 return 148 } 149 } 150 if rc.Request != nil { 151 if rc.Request.URL != nil { 152 value = rc.Request.URL.Query().Get(name) 153 if value != "" { 154 return 155 } 156 } 157 if rc.Request.Header != nil { 158 value = rc.Request.Header.Get(name) 159 if value != "" { 160 return 161 } 162 } 163 164 value, err = rc.FormValue(name) 165 if err == nil { 166 return 167 } 168 169 var cookie *http.Cookie 170 cookie, err = rc.Request.Cookie(name) 171 if err == nil && cookie.Value != "" { 172 value = cookie.Value 173 return 174 } 175 } 176 177 err = NewParameterMissingError(name) 178 return 179 } 180 181 // RouteParam returns a string route parameter 182 func (rc *Ctx) RouteParam(key string) (output string, err error) { 183 if value, hasKey := rc.RouteParams[key]; hasKey { 184 output = value 185 return 186 } 187 err = NewParameterMissingError(key) 188 return 189 } 190 191 // QueryValue returns a query value. 192 func (rc *Ctx) QueryValue(key string) (value string, err error) { 193 if value = rc.Request.URL.Query().Get(key); len(value) > 0 { 194 return 195 } 196 err = NewParameterMissingError(key) 197 return 198 } 199 200 // FormValue returns a form value. 201 func (rc *Ctx) FormValue(key string) (output string, err error) { 202 if err = rc.EnsureForm(); err != nil { 203 return 204 } 205 if value := rc.Form.Get(key); len(value) > 0 { 206 output = value 207 return 208 } 209 err = NewParameterMissingError(key) 210 return 211 } 212 213 // HeaderValue returns a header value. 214 func (rc *Ctx) HeaderValue(key string) (value string, err error) { 215 if value = rc.Request.Header.Get(key); len(value) > 0 { 216 return 217 } 218 err = NewParameterMissingError(key) 219 return 220 } 221 222 // PostBody reads, caches and returns the bytes on a request post body. 223 // It will store those bytes for re-use on this context object. 224 // If you're expecting a large post body, or a large post body is even possible 225 // use a stream reader on `.Request.Body` instead of this method. 226 func (rc *Ctx) PostBody() ([]byte, error) { 227 if len(rc.Body) == 0 { 228 if rc.Request != nil && rc.Request.GetBody != nil { 229 reader, err := rc.Request.GetBody() 230 if err != nil { 231 return nil, ex.New(err) 232 } 233 defer reader.Close() 234 rc.Body, err = io.ReadAll(reader) 235 if err != nil { 236 return nil, ex.New(err) 237 } 238 } 239 if rc.Request != nil && rc.Request.Body != nil { 240 defer rc.Request.Body.Close() 241 var err error 242 rc.Body, err = io.ReadAll(rc.Request.Body) 243 if err != nil { 244 return nil, ex.New(err) 245 } 246 } 247 } 248 return rc.Body, nil 249 } 250 251 // PostBodyAsString returns the post body as a string. 252 func (rc *Ctx) PostBodyAsString() (string, error) { 253 body, err := rc.PostBody() 254 if err != nil { 255 return "", err 256 } 257 return string(body), nil 258 } 259 260 // PostBodyAsJSON reads the incoming post body (closing it) and marshals it to the target object as json. 261 func (rc *Ctx) PostBodyAsJSON(response interface{}) error { 262 body, err := rc.PostBody() 263 if err != nil { 264 return err 265 } 266 if err = json.Unmarshal(body, response); err != nil { 267 return ex.New(err) 268 } 269 return nil 270 } 271 272 // PostBodyAsXML reads the incoming post body (closing it) and marshals it to the target object as xml. 273 func (rc *Ctx) PostBodyAsXML(response interface{}) error { 274 body, err := rc.PostBody() 275 if err != nil { 276 return err 277 } 278 if err = xml.Unmarshal(body, response); err != nil { 279 return ex.New(err) 280 } 281 return nil 282 } 283 284 // PostBodyAsForm reads the incoming post body (closing it) sets a given object from the post form fields. 285 // NOTE: the request method *MUST* not be `GET` otherwise the golang internals will skip parsing the body. 286 func (rc *Ctx) PostBodyAsForm(response interface{}) error { 287 if err := rc.EnsureForm(); err != nil { 288 return err 289 } 290 return reflectutil.PatchStringsFunc("postForm", func(key string) (string, bool) { 291 if values, ok := rc.Form[key]; ok { 292 if len(values) > 0 { 293 return values[0], true 294 } 295 return "", false 296 } 297 return "", false 298 }, response) 299 } 300 301 // Cookie returns a named cookie from the request. 302 func (rc *Ctx) Cookie(name string) *http.Cookie { 303 cookie, err := rc.Request.Cookie(name) 304 if err != nil { 305 return nil 306 } 307 return cookie 308 } 309 310 // ExtendCookieByDuration extends a cookie by a time duration (on the order of nanoseconds to hours). 311 func (rc *Ctx) ExtendCookieByDuration(name string, path string, duration time.Duration) { 312 c := rc.Cookie(name) 313 if c == nil { 314 return 315 } 316 c.Path = path 317 if c.Expires.IsZero() { 318 c.Expires = time.Now().UTC().Add(duration) 319 } else { 320 c.Expires = c.Expires.Add(duration) 321 } 322 http.SetCookie(rc.Response, c) 323 } 324 325 // ExtendCookie extends a cookie by years, months or days. 326 func (rc *Ctx) ExtendCookie(name string, path string, years, months, days int) { 327 c := rc.Cookie(name) 328 if c == nil { 329 return 330 } 331 c.Path = path 332 if c.Expires.IsZero() { 333 c.Expires = time.Now().UTC().AddDate(years, months, days) 334 } else { 335 c.Expires = c.Expires.AddDate(years, months, days) 336 } 337 http.SetCookie(rc.Response, c) 338 } 339 340 // ExpireCookie expires a cookie. 341 func (rc *Ctx) ExpireCookie(name string, path string) { 342 c := rc.Cookie(name) 343 if c == nil { 344 return 345 } 346 c.Path = path 347 c.Value = NewSessionID() 348 // c.MaxAge<0 means delete cookie now, and is equivalent to 349 // the literal cookie header content 'Max-Age: 0' 350 c.MaxAge = -1 351 http.SetCookie(rc.Response, c) 352 } 353 354 // Elapsed is the time delta between start and end. 355 func (rc *Ctx) Elapsed() time.Duration { 356 return time.Now().UTC().Sub(rc.RequestStarted) 357 } 358 359 // -------------------------------------------------------------------------------- 360 // internal methods 361 // -------------------------------------------------------------------------------- 362 363 // EnsureForm parses the post body as an application form. 364 // The parsed form will be available on the `.Form` field. 365 func (rc *Ctx) EnsureForm() error { 366 if rc.Form != nil { 367 return nil 368 } 369 if rc.Request.PostForm != nil { 370 rc.Form = rc.Request.PostForm 371 return nil 372 } 373 374 body, err := rc.PostBody() 375 if err != nil { 376 return err 377 } 378 r := &http.Request{ 379 Method: rc.Request.Method, 380 Header: rc.Request.Header, 381 Body: io.NopCloser(bytes.NewBuffer(body)), 382 } 383 if err := r.ParseForm(); err != nil { 384 return err 385 } 386 rc.Form = r.PostForm 387 return nil 388 } 389 390 // Labels returns the labels for logging calls. 391 func (rc *Ctx) Labels() map[string]string { 392 fields := make(map[string]string) 393 if rc.Route != nil { 394 fields["web.route"] = rc.Route.String() 395 } 396 if rc.Session != nil { 397 fields["web.user"] = rc.Session.UserID 398 } 399 return fields 400 } 401 402 // Annotations returns the annotations for logging calls. 403 func (rc *Ctx) Annotations() map[string]interface{} { 404 fields := make(map[string]interface{}) 405 if len(rc.RouteParams) > 0 { 406 fields["web.route_parameters"] = rc.RouteParams 407 } 408 return fields 409 }