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  }