From 9b2d7d6d69989a749e59e28354af448a65ffbd2a Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Thu, 8 Jul 2021 14:19:40 +0200 Subject: [PATCH] Gradually remove Macaron web framework (#36325) * add macaron code to the code base * remove unused secure cookies support from macaron * clean up modules * remove com dependency * fix silly typos * little cleanup, remove recovery middleware * remove logger middleware * remove static handler and remove unused context methods * bring inject into macaron codebase * remove unused applicator * add back macaron license * more cleanups in macaron code * remove unused injector Set method * remove unused context methods: param to int conversion, body helper type, cookie helpers * remove action from context * remove complex environment handling, we only use Env variable * restore ReplaceAllParams to fix the tests --- go.mod | 2 + pkg/api/dashboard.go | 2 +- pkg/macaron/LICENSE.txt | 191 ++++++++ pkg/macaron/context.go | 226 +++++++++ pkg/macaron/go.mod | 3 + pkg/macaron/inject.go | 208 +++++++++ pkg/macaron/macaron.go | 211 +++++++++ pkg/macaron/render.go | 723 +++++++++++++++++++++++++++++ pkg/macaron/response_writer.go | 124 +++++ pkg/macaron/return_handler.go | 74 +++ pkg/macaron/router.go | 380 +++++++++++++++ pkg/macaron/tree.go | 390 ++++++++++++++++ pkg/services/live/pushhttp/push.go | 3 +- 13 files changed, 2535 insertions(+), 2 deletions(-) create mode 100644 pkg/macaron/LICENSE.txt create mode 100644 pkg/macaron/context.go create mode 100644 pkg/macaron/go.mod create mode 100644 pkg/macaron/inject.go create mode 100644 pkg/macaron/macaron.go create mode 100644 pkg/macaron/render.go create mode 100644 pkg/macaron/response_writer.go create mode 100644 pkg/macaron/return_handler.go create mode 100644 pkg/macaron/router.go create mode 100644 pkg/macaron/tree.go diff --git a/go.mod b/go.mod index ccd067c7e3c..ee016ea3a9a 100644 --- a/go.mod +++ b/go.mod @@ -121,3 +121,5 @@ require ( ) replace github.com/apache/thrift => github.com/apache/thrift v0.14.1 + +replace gopkg.in/macaron.v1 => ./pkg/macaron diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 91a9f2d13cd..95a88fd4081 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -553,7 +553,7 @@ func GetDashboardVersion(c *models.ReqContext) response.Response { query := models.GetDashboardVersionQuery{ OrgId: c.OrgId, DashboardId: dashID, - Version: c.ParamsInt(":id"), + Version: int(c.ParamsInt64(":id")), } if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/macaron/LICENSE.txt b/pkg/macaron/LICENSE.txt new file mode 100644 index 00000000000..c8a16eb2eb9 --- /dev/null +++ b/pkg/macaron/LICENSE.txt @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright 2014 The Macaron Authors + + 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 + + http://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. diff --git a/pkg/macaron/context.go b/pkg/macaron/context.go new file mode 100644 index 00000000000..2920f834af8 --- /dev/null +++ b/pkg/macaron/context.go @@ -0,0 +1,226 @@ +// Copyright 2014 The Macaron Authors +// +// 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 +// +// http://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 macaron + +import ( + "net/http" + "net/url" + "reflect" + "strconv" + "strings" +) + +// Request represents an HTTP request received by a server or to be sent by a client. +type Request struct { + *http.Request +} + +// ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context). +type ContextInvoker func(ctx *Context) + +// Invoke implements inject.FastInvoker which simplifies calls of `func(ctx *Context)` function. +func (invoke ContextInvoker) Invoke(params []interface{}) ([]reflect.Value, error) { + invoke(params[0].(*Context)) + return nil, nil +} + +// Context represents the runtime context of current request of Macaron instance. +// It is the integration of most frequently used middlewares and helper methods. +type Context struct { + Injector + handlers []Handler + index int + + *Router + Req Request + Resp ResponseWriter + params Params + Render + Data map[string]interface{} +} + +func (ctx *Context) handler() Handler { + if ctx.index < len(ctx.handlers) { + return ctx.handlers[ctx.index] + } + if ctx.index == len(ctx.handlers) { + return func() {} + } + panic("invalid index for context handler") +} + +// Next runs the next handler in the context chain +func (ctx *Context) Next() { + ctx.index++ + ctx.run() +} + +// Written returns whether the context response has been written to +func (ctx *Context) Written() bool { + return ctx.Resp.Written() +} + +func (ctx *Context) run() { + for ctx.index <= len(ctx.handlers) { + vals, err := ctx.Invoke(ctx.handler()) + if err != nil { + panic(err) + } + ctx.index++ + + // if the handler returned something, write it to the http response + if len(vals) > 0 { + ev := ctx.GetVal(reflect.TypeOf(ReturnHandler(nil))) + handleReturn := ev.Interface().(ReturnHandler) + handleReturn(ctx, vals) + } + + if ctx.Written() { + return + } + } +} + +// RemoteAddr returns more real IP address. +func (ctx *Context) RemoteAddr() string { + addr := ctx.Req.Header.Get("X-Real-IP") + if len(addr) == 0 { + addr = ctx.Req.Header.Get("X-Forwarded-For") + if addr == "" { + addr = ctx.Req.RemoteAddr + if i := strings.LastIndex(addr, ":"); i > -1 { + addr = addr[:i] + } + } + } + return addr +} + +func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) { + if len(data) <= 0 { + ctx.Render.HTMLSet(status, setName, tplName, ctx.Data) + } else if len(data) == 1 { + ctx.Render.HTMLSet(status, setName, tplName, data[0]) + } else { + ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions)) + } +} + +// HTML renders the HTML with default template set. +func (ctx *Context) HTML(status int, name string, data ...interface{}) { + ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...) +} + +// Redirect sends a redirect response +func (ctx *Context) Redirect(location string, status ...int) { + code := http.StatusFound + if len(status) == 1 { + code = status[0] + } + + http.Redirect(ctx.Resp, ctx.Req.Request, location, code) +} + +// MaxMemory is the maximum amount of memory to use when parsing a multipart form. +// Set this to whatever value you prefer; default is 10 MB. +var MaxMemory = int64(1024 * 1024 * 10) + +func (ctx *Context) parseForm() { + if ctx.Req.Form != nil { + return + } + + contentType := ctx.Req.Header.Get(_CONTENT_TYPE) + if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") && + len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") { + _ = ctx.Req.ParseMultipartForm(MaxMemory) + } else { + _ = ctx.Req.ParseForm() + } +} + +// Query querys form parameter. +func (ctx *Context) Query(name string) string { + ctx.parseForm() + return ctx.Req.Form.Get(name) +} + +// QueryStrings returns a list of results by given query name. +func (ctx *Context) QueryStrings(name string) []string { + ctx.parseForm() + + vals, ok := ctx.Req.Form[name] + if !ok { + return []string{} + } + return vals +} + +// QueryBool returns query result in bool type. +func (ctx *Context) QueryBool(name string) bool { + v, _ := strconv.ParseBool(ctx.Query(name)) + return v +} + +// QueryInt returns query result in int type. +func (ctx *Context) QueryInt(name string) int { + n, _ := strconv.Atoi(ctx.Query(name)) + return n +} + +// QueryInt64 returns query result in int64 type. +func (ctx *Context) QueryInt64(name string) int64 { + n, _ := strconv.ParseInt(ctx.Query(name), 10, 64) + return n +} + +// Params returns value of given param name. +// e.g. ctx.Params(":uid") or ctx.Params("uid") +func (ctx *Context) Params(name string) string { + if len(name) == 0 { + return "" + } + if len(name) > 1 && name[0] != ':' { + name = ":" + name + } + return ctx.params[name] +} + +// AllParams returns all params. +func (ctx *Context) AllParams() Params { + return ctx.params +} + +// ReplaceAllParams replace all current params with given params +func (ctx *Context) ReplaceAllParams(params Params) { + ctx.params = params +} + +// ParamsInt64 returns params result in int64 type. +// e.g. ctx.ParamsInt64(":uid") +func (ctx *Context) ParamsInt64(name string) int64 { + n, _ := strconv.ParseInt(ctx.Params(name), 10, 64) + return n +} + +// GetCookie returns given cookie value from request header. +func (ctx *Context) GetCookie(name string) string { + cookie, err := ctx.Req.Cookie(name) + if err != nil { + return "" + } + val, _ := url.QueryUnescape(cookie.Value) + return val +} diff --git a/pkg/macaron/go.mod b/pkg/macaron/go.mod new file mode 100644 index 00000000000..66520a9f4d5 --- /dev/null +++ b/pkg/macaron/go.mod @@ -0,0 +1,3 @@ +module gopkg.in/macaron.v1 + +go 1.16 diff --git a/pkg/macaron/inject.go b/pkg/macaron/inject.go new file mode 100644 index 00000000000..5a7cf9c9bc6 --- /dev/null +++ b/pkg/macaron/inject.go @@ -0,0 +1,208 @@ +// Copyright 2013 Jeremy Saenz +// Copyright 2015 The Macaron Authors +// +// 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 +// +// http://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 inject provides utilities for mapping and injecting dependencies in various ways. +package macaron + +import ( + "fmt" + "reflect" +) + +// Injector represents an interface for mapping and injecting dependencies into structs +// and function arguments. +type Injector interface { + Invoker + TypeMapper + // SetParent sets the parent of the injector. If the injector cannot find a + // dependency in its Type map it will check its parent before returning an + // error. + SetParent(Injector) +} + +// Invoker represents an interface for calling functions via reflection. +type Invoker interface { + // Invoke attempts to call the interface{} provided as a function, + // providing dependencies for function arguments based on Type. Returns + // a slice of reflect.Value representing the returned values of the function. + // Returns an error if the injection fails. + Invoke(interface{}) ([]reflect.Value, error) +} + +// FastInvoker represents an interface in order to avoid the calling function via reflection. +// +// example: +// type handlerFuncHandler func(http.ResponseWriter, *http.Request) error +// func (f handlerFuncHandler)Invoke([]interface{}) ([]reflect.Value, error){ +// ret := f(p[0].(http.ResponseWriter), p[1].(*http.Request)) +// return []reflect.Value{reflect.ValueOf(ret)}, nil +// } +// +// type funcHandler func(int, string) +// func (f funcHandler)Invoke([]interface{}) ([]reflect.Value, error){ +// f(p[0].(int), p[1].(string)) +// return nil, nil +// } +type FastInvoker interface { + // Invoke attempts to call the ordinary functions. If f is a function + // with the appropriate signature, f.Invoke([]interface{}) is a Call that calls f. + // Returns a slice of reflect.Value representing the returned values of the function. + // Returns an error if the injection fails. + Invoke([]interface{}) ([]reflect.Value, error) +} + +// IsFastInvoker check interface is FastInvoker +func IsFastInvoker(h interface{}) bool { + _, ok := h.(FastInvoker) + return ok +} + +// TypeMapper represents an interface for mapping interface{} values based on type. +type TypeMapper interface { + // Maps the interface{} value based on its immediate type from reflect.TypeOf. + Map(interface{}) TypeMapper + // Maps the interface{} value based on the pointer of an Interface provided. + // This is really only useful for mapping a value as an interface, as interfaces + // cannot at this time be referenced directly without a pointer. + MapTo(interface{}, interface{}) TypeMapper + // Returns the Value that is mapped to the current type. Returns a zeroed Value if + // the Type has not been mapped. + GetVal(reflect.Type) reflect.Value +} + +type injector struct { + values map[reflect.Type]reflect.Value + parent Injector +} + +// InterfaceOf dereferences a pointer to an Interface type. +// It panics if value is not an pointer to an interface. +func InterfaceOf(value interface{}) reflect.Type { + t := reflect.TypeOf(value) + + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Interface { + panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)") + } + + return t +} + +// New returns a new Injector. +func NewInjector() Injector { + return &injector{ + values: make(map[reflect.Type]reflect.Value), + } +} + +// Invoke attempts to call the interface{} provided as a function, +// providing dependencies for function arguments based on Type. +// Returns a slice of reflect.Value representing the returned values of the function. +// Returns an error if the injection fails. +// It panics if f is not a function +func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) { + t := reflect.TypeOf(f) + switch v := f.(type) { + case FastInvoker: + return inj.fastInvoke(v, t, t.NumIn()) + default: + return inj.callInvoke(f, t, t.NumIn()) + } +} + +func (inj *injector) fastInvoke(f FastInvoker, t reflect.Type, numIn int) ([]reflect.Value, error) { + var in []interface{} + if numIn > 0 { + in = make([]interface{}, numIn) // Panic if t is not kind of Func + var argType reflect.Type + var val reflect.Value + for i := 0; i < numIn; i++ { + argType = t.In(i) + val = inj.GetVal(argType) + if !val.IsValid() { + return nil, fmt.Errorf("Value not found for type %v", argType) + } + + in[i] = val.Interface() + } + } + return f.Invoke(in) +} + +// callInvoke reflect.Value.Call +func (inj *injector) callInvoke(f interface{}, t reflect.Type, numIn int) ([]reflect.Value, error) { + var in []reflect.Value + if numIn > 0 { + in = make([]reflect.Value, numIn) + var argType reflect.Type + var val reflect.Value + for i := 0; i < numIn; i++ { + argType = t.In(i) + val = inj.GetVal(argType) + if !val.IsValid() { + return nil, fmt.Errorf("Value not found for type %v", argType) + } + + in[i] = val + } + } + return reflect.ValueOf(f).Call(in), nil +} + +// Maps the concrete value of val to its dynamic type using reflect.TypeOf, +// It returns the TypeMapper registered in. +func (i *injector) Map(val interface{}) TypeMapper { + i.values[reflect.TypeOf(val)] = reflect.ValueOf(val) + return i +} + +func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper { + i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val) + return i +} + +func (i *injector) GetVal(t reflect.Type) reflect.Value { + val := i.values[t] + + if val.IsValid() { + return val + } + + // no concrete types found, try to find implementors + // if t is an interface + if t.Kind() == reflect.Interface { + for k, v := range i.values { + if k.Implements(t) { + val = v + break + } + } + } + + // Still no type found, try to look it up on the parent + if !val.IsValid() && i.parent != nil { + val = i.parent.GetVal(t) + } + + return val + +} + +func (i *injector) SetParent(parent Injector) { + i.parent = parent +} diff --git a/pkg/macaron/macaron.go b/pkg/macaron/macaron.go new file mode 100644 index 00000000000..c3957cc47cd --- /dev/null +++ b/pkg/macaron/macaron.go @@ -0,0 +1,211 @@ +// +build go1.3 + +// Copyright 2014 The Macaron Authors +// +// 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 +// +// http://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 macaron is a high productive and modular web framework in Go. +package macaron // import "gopkg.in/macaron.v1" + +import ( + "log" + "net/http" + "os" + "reflect" + "strconv" + "strings" +) + +const _VERSION = "1.3.4.0805" + +const ( + DEV = "development" + PROD = "production" +) + +var ( + // Env is the environment that Macaron is executing in. + // The MACARON_ENV is read on initialization to set this variable. + Env = DEV +) + +func Version() string { + return _VERSION +} + +// Handler can be any callable function. +// Macaron attempts to inject services into the handler's argument list, +// and panics if an argument could not be fullfilled via dependency injection. +type Handler interface{} + +// handlerFuncInvoker is an inject.FastInvoker wrapper of func(http.ResponseWriter, *http.Request). +type handlerFuncInvoker func(http.ResponseWriter, *http.Request) + +func (invoke handlerFuncInvoker) Invoke(params []interface{}) ([]reflect.Value, error) { + invoke(params[0].(http.ResponseWriter), params[1].(*http.Request)) + return nil, nil +} + +// internalServerErrorInvoker is an inject.FastInvoker wrapper of func(rw http.ResponseWriter, err error). +type internalServerErrorInvoker func(rw http.ResponseWriter, err error) + +func (invoke internalServerErrorInvoker) Invoke(params []interface{}) ([]reflect.Value, error) { + invoke(params[0].(http.ResponseWriter), params[1].(error)) + return nil, nil +} + +// validateAndWrapHandler makes sure a handler is a callable function, it panics if not. +// When the handler is also potential to be any built-in inject.FastInvoker, +// it wraps the handler automatically to have some performance gain. +func validateAndWrapHandler(h Handler) Handler { + if reflect.TypeOf(h).Kind() != reflect.Func { + panic("Macaron handler must be a callable function") + } + + if !IsFastInvoker(h) { + switch v := h.(type) { + case func(*Context): + return ContextInvoker(v) + case func(http.ResponseWriter, *http.Request): + return handlerFuncInvoker(v) + case func(http.ResponseWriter, error): + return internalServerErrorInvoker(v) + } + } + return h +} + +// validateAndWrapHandlers preforms validation and wrapping for each input handler. +// It accepts an optional wrapper function to perform custom wrapping on handlers. +func validateAndWrapHandlers(handlers []Handler, wrappers ...func(Handler) Handler) []Handler { + var wrapper func(Handler) Handler + if len(wrappers) > 0 { + wrapper = wrappers[0] + } + + wrappedHandlers := make([]Handler, len(handlers)) + for i, h := range handlers { + h = validateAndWrapHandler(h) + if wrapper != nil && !IsFastInvoker(h) { + h = wrapper(h) + } + wrappedHandlers[i] = h + } + + return wrappedHandlers +} + +// Macaron represents the top level web application. +// Injector methods can be invoked to map services on a global level. +type Macaron struct { + Injector + befores []BeforeHandler + handlers []Handler + + hasURLPrefix bool + urlPrefix string // For suburl support. + *Router + + logger *log.Logger +} + +// New creates a bare bones Macaron instance. +// Use this method if you want to have full control over the middleware that is used. +func New() *Macaron { + m := &Macaron{ + Injector: NewInjector(), + Router: NewRouter(), + logger: log.New(os.Stdout, "[Macaron] ", 0), + } + m.Router.m = m + m.Map(m.logger) + m.Map(defaultReturnHandler()) + m.NotFound(http.NotFound) + m.InternalServerError(func(rw http.ResponseWriter, err error) { + http.Error(rw, err.Error(), 500) + }) + return m +} + +// Handlers sets the entire middleware stack with the given Handlers. +// This will clear any current middleware handlers, +// and panics if any of the handlers is not a callable function +func (m *Macaron) Handlers(handlers ...Handler) { + m.handlers = make([]Handler, 0) + for _, handler := range handlers { + m.Use(handler) + } +} + +// BeforeHandler represents a handler executes at beginning of every request. +// Macaron stops future process when it returns true. +type BeforeHandler func(rw http.ResponseWriter, req *http.Request) bool + +// Use adds a middleware Handler to the stack, +// and panics if the handler is not a callable func. +// Middleware Handlers are invoked in the order that they are added. +func (m *Macaron) Use(handler Handler) { + handler = validateAndWrapHandler(handler) + m.handlers = append(m.handlers, handler) +} + +func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context { + c := &Context{ + Injector: NewInjector(), + handlers: m.handlers, + index: 0, + Router: m.Router, + Req: Request{req}, + Resp: NewResponseWriter(req.Method, rw), + Render: &DummyRender{rw}, + Data: make(map[string]interface{}), + } + c.SetParent(m) + c.Map(c) + c.MapTo(c.Resp, (*http.ResponseWriter)(nil)) + c.Map(req) + return c +} + +// ServeHTTP is the HTTP Entry point for a Macaron instance. +// Useful if you want to control your own HTTP server. +// Be aware that none of middleware will run without registering any router. +func (m *Macaron) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if m.hasURLPrefix { + req.URL.Path = strings.TrimPrefix(req.URL.Path, m.urlPrefix) + } + for _, h := range m.befores { + if h(rw, req) { + return + } + } + m.Router.ServeHTTP(rw, req) +} + +func getDefaultListenInfo() (string, int) { + host := os.Getenv("HOST") + if len(host) == 0 { + host = "0.0.0.0" + } + port, _ := strconv.Atoi(os.Getenv("PORT")) + if port == 0 { + port = 4000 + } + return host, port +} + +// SetURLPrefix sets URL prefix of router layer, so that it support suburl. +func (m *Macaron) SetURLPrefix(prefix string) { + m.urlPrefix = prefix + m.hasURLPrefix = len(m.urlPrefix) > 0 +} diff --git a/pkg/macaron/render.go b/pkg/macaron/render.go new file mode 100644 index 00000000000..8aec82bee59 --- /dev/null +++ b/pkg/macaron/render.go @@ -0,0 +1,723 @@ +// Copyright 2013 Martini Authors +// Copyright 2014 The Macaron Authors +// +// 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 +// +// http://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 macaron + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "html/template" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "sync" + "time" +) + +const ( + _CONTENT_TYPE = "Content-Type" + _CONTENT_BINARY = "application/octet-stream" + _CONTENT_JSON = "application/json" + _CONTENT_HTML = "text/html" + _CONTENT_PLAIN = "text/plain" + _CONTENT_XHTML = "application/xhtml+xml" + _CONTENT_XML = "text/xml" + _DEFAULT_CHARSET = "UTF-8" +) + +var ( + // Provides a temporary buffer to execute templates into and catch errors. + bufpool = sync.Pool{ + New: func() interface{} { return new(bytes.Buffer) }, + } + + // Included helper functions for use when rendering html + helperFuncs = template.FuncMap{ + "yield": func() (string, error) { + return "", fmt.Errorf("yield called with no layout defined") + }, + "current": func() (string, error) { + return "", nil + }, + } +) + +type ( + // TemplateFile represents a interface of template file that has name and can be read. + TemplateFile interface { + Name() string + Data() []byte + Ext() string + } + // TemplateFileSystem represents a interface of template file system that able to list all files. + TemplateFileSystem interface { + ListFiles() []TemplateFile + Get(string) (io.Reader, error) + } + + // Delims represents a set of Left and Right delimiters for HTML template rendering + Delims struct { + // Left delimiter, defaults to {{ + Left string + // Right delimiter, defaults to }} + Right string + } + + // RenderOptions represents a struct for specifying configuration options for the Render middleware. + RenderOptions struct { + // Directory to load templates. Default is "templates". + Directory string + // Addtional directories to overwite templates. + AppendDirectories []string + // Layout template name. Will not render a layout if "". Default is to "". + Layout string + // Extensions to parse template files from. Defaults are [".tmpl", ".html"]. + Extensions []string + // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is []. + Funcs []template.FuncMap + // Delims sets the action delimiters to the specified strings in the Delims struct. + Delims Delims + // Appends the given charset to the Content-Type header. Default is "UTF-8". + Charset string + // Outputs human readable JSON. + IndentJSON bool + // Outputs human readable XML. + IndentXML bool + // Prefixes the JSON output with the given bytes. + PrefixJSON []byte + // Prefixes the XML output with the given bytes. + PrefixXML []byte + // Allows changing of output to XHTML instead of HTML. Default is "text/html" + HTMLContentType string + // TemplateFileSystem is the interface for supporting any implmentation of template file system. + TemplateFileSystem + } + + // HTMLOptions is a struct for overriding some rendering Options for specific HTML call + HTMLOptions struct { + // Layout template name. Overrides Options.Layout. + Layout string + } + + Render interface { + http.ResponseWriter + SetResponseWriter(http.ResponseWriter) + + JSON(int, interface{}) + JSONString(interface{}) (string, error) + RawData(int, []byte) // Serve content as binary + PlainText(int, []byte) // Serve content as plain text + HTML(int, string, interface{}, ...HTMLOptions) + HTMLSet(int, string, string, interface{}, ...HTMLOptions) + HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) + HTMLString(string, interface{}, ...HTMLOptions) (string, error) + HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) + HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) + XML(int, interface{}) + Error(int, ...string) + Status(int) + SetTemplatePath(string, string) + HasTemplateSet(string) bool + } +) + +// TplFile implements TemplateFile interface. +type TplFile struct { + name string + data []byte + ext string +} + +// NewTplFile cerates new template file with given name and data. +func NewTplFile(name string, data []byte, ext string) *TplFile { + return &TplFile{name, data, ext} +} + +func (f *TplFile) Name() string { + return f.name +} + +func (f *TplFile) Data() []byte { + return f.data +} + +func (f *TplFile) Ext() string { + return f.ext +} + +// TplFileSystem implements TemplateFileSystem interface. +type TplFileSystem struct { + files []TemplateFile +} + +// NewTemplateFileSystem creates new template file system with given options. +func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem { + fs := TplFileSystem{} + fs.files = make([]TemplateFile, 0, 10) + + // Directories are composed in reverse order because later one overwrites previous ones, + // so once found, we can directly jump out of the loop. + dirs := make([]string, 0, len(opt.AppendDirectories)+1) + for i := len(opt.AppendDirectories) - 1; i >= 0; i-- { + dirs = append(dirs, opt.AppendDirectories[i]) + } + dirs = append(dirs, opt.Directory) + + var err error + for i := range dirs { + // Skip ones that does not exists for symlink test, + // but allow non-symlink ones added after start. + if _, err := os.Stat(dirs[i]); err != nil && os.IsNotExist(err) { + continue + } + + dirs[i], err = filepath.EvalSymlinks(dirs[i]) + if err != nil { + panic("EvalSymlinks(" + dirs[i] + "): " + err.Error()) + } + } + lastDir := dirs[len(dirs)-1] + + // We still walk the last (original) directory because it's non-sense we load templates not exist in original directory. + if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error { + r, err := filepath.Rel(lastDir, path) + if err != nil { + return err + } + + ext := GetExt(r) + + for _, extension := range opt.Extensions { + if ext != extension { + continue + } + + var data []byte + if !omitData { + // Loop over candidates of directory, break out once found. + // The file always exists because it's inside the walk function, + // and read original file is the worst case. + for i := range dirs { + path = filepath.Join(dirs[i], r) + if f, err := os.Stat(path); err != nil || f.IsDir() { + continue + } + + data, err = ioutil.ReadFile(path) + if err != nil { + return err + } + break + } + } + + name := filepath.ToSlash((r[0 : len(r)-len(ext)])) + fs.files = append(fs.files, NewTplFile(name, data, ext)) + } + + return nil + }); err != nil { + panic("NewTemplateFileSystem: " + err.Error()) + } + + return fs +} + +func (fs TplFileSystem) ListFiles() []TemplateFile { + return fs.files +} + +func (fs TplFileSystem) Get(name string) (io.Reader, error) { + for i := range fs.files { + if fs.files[i].Name()+fs.files[i].Ext() == name { + return bytes.NewReader(fs.files[i].Data()), nil + } + } + return nil, fmt.Errorf("file '%s' not found", name) +} + +func PrepareCharset(charset string) string { + if len(charset) != 0 { + return "; charset=" + charset + } + + return "; charset=" + _DEFAULT_CHARSET +} + +func GetExt(s string) string { + index := strings.Index(s, ".") + if index == -1 { + return "" + } + return s[index:] +} + +func compile(opt RenderOptions) *template.Template { + t := template.New(opt.Directory) + t.Delims(opt.Delims.Left, opt.Delims.Right) + // Parse an initial template in case we don't have any. + template.Must(t.Parse("Macaron")) + + if opt.TemplateFileSystem == nil { + opt.TemplateFileSystem = NewTemplateFileSystem(opt, false) + } + + for _, f := range opt.TemplateFileSystem.ListFiles() { + tmpl := t.New(f.Name()) + for _, funcs := range opt.Funcs { + tmpl.Funcs(funcs) + } + // Bomb out if parse fails. We don't want any silent server starts. + template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data()))) + } + + return t +} + +const ( + DEFAULT_TPL_SET_NAME = "DEFAULT" +) + +// TemplateSet represents a template set of type *template.Template. +type TemplateSet struct { + lock sync.RWMutex + sets map[string]*template.Template + dirs map[string]string +} + +// NewTemplateSet initializes a new empty template set. +func NewTemplateSet() *TemplateSet { + return &TemplateSet{ + sets: make(map[string]*template.Template), + dirs: make(map[string]string), + } +} + +func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template { + t := compile(*opt) + + ts.lock.Lock() + defer ts.lock.Unlock() + + ts.sets[name] = t + ts.dirs[name] = opt.Directory + return t +} + +func (ts *TemplateSet) Get(name string) *template.Template { + ts.lock.RLock() + defer ts.lock.RUnlock() + + return ts.sets[name] +} + +func (ts *TemplateSet) GetDir(name string) string { + ts.lock.RLock() + defer ts.lock.RUnlock() + + return ts.dirs[name] +} + +func prepareRenderOptions(options []RenderOptions) RenderOptions { + var opt RenderOptions + if len(options) > 0 { + opt = options[0] + } + + // Defaults. + if len(opt.Directory) == 0 { + opt.Directory = "templates" + } + if len(opt.Extensions) == 0 { + opt.Extensions = []string{".tmpl", ".html"} + } + if len(opt.HTMLContentType) == 0 { + opt.HTMLContentType = _CONTENT_HTML + } + + return opt +} + +func ParseTplSet(tplSet string) (tplName string, tplDir string) { + tplSet = strings.TrimSpace(tplSet) + if len(tplSet) == 0 { + panic("empty template set argument") + } + infos := strings.Split(tplSet, ":") + if len(infos) == 1 { + tplDir = infos[0] + tplName = path.Base(tplDir) + } else { + tplName = infos[0] + tplDir = infos[1] + } + + dir, err := os.Stat(tplDir) + if err != nil || !dir.IsDir() { + panic("template set path does not exist or is not a directory") + } + return tplName, tplDir +} + +func renderHandler(opt RenderOptions, tplSets []string) Handler { + cs := PrepareCharset(opt.Charset) + ts := NewTemplateSet() + ts.Set(DEFAULT_TPL_SET_NAME, &opt) + + var tmpOpt RenderOptions + for _, tplSet := range tplSets { + tplName, tplDir := ParseTplSet(tplSet) + tmpOpt = opt + tmpOpt.Directory = tplDir + ts.Set(tplName, &tmpOpt) + } + + return func(ctx *Context) { + r := &TplRender{ + ResponseWriter: ctx.Resp, + TemplateSet: ts, + Opt: &opt, + CompiledCharset: cs, + } + ctx.Data["TmplLoadTimes"] = func() string { + if r.startTime.IsZero() { + return "" + } + return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms" + } + + ctx.Render = r + ctx.MapTo(r, (*Render)(nil)) + } +} + +// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain. +// An single variadic macaron.RenderOptions struct can be optionally provided to configure +// HTML rendering. The default directory for templates is "templates" and the default +// file extension is ".tmpl" and ".html". +// +// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the +// MACARON_ENV environment variable to "production". +func Renderer(options ...RenderOptions) Handler { + return renderHandler(prepareRenderOptions(options), []string{}) +} + +func Renderers(options RenderOptions, tplSets ...string) Handler { + return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets) +} + +type TplRender struct { + http.ResponseWriter + *TemplateSet + Opt *RenderOptions + CompiledCharset string + + startTime time.Time +} + +func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) { + r.ResponseWriter = rw +} + +func (r *TplRender) JSON(status int, v interface{}) { + var ( + result []byte + err error + ) + if r.Opt.IndentJSON { + result, err = json.MarshalIndent(v, "", " ") + } else { + result, err = json.Marshal(v) + } + if err != nil { + http.Error(r, err.Error(), 500) + return + } + + // json rendered fine, write out the result + r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset) + r.WriteHeader(status) + if len(r.Opt.PrefixJSON) > 0 { + _, _ = r.Write(r.Opt.PrefixJSON) + } + _, _ = r.Write(result) +} + +func (r *TplRender) JSONString(v interface{}) (string, error) { + var result []byte + var err error + if r.Opt.IndentJSON { + result, err = json.MarshalIndent(v, "", " ") + } else { + result, err = json.Marshal(v) + } + if err != nil { + return "", err + } + return string(result), nil +} + +func (r *TplRender) XML(status int, v interface{}) { + var result []byte + var err error + if r.Opt.IndentXML { + result, err = xml.MarshalIndent(v, "", " ") + } else { + result, err = xml.Marshal(v) + } + if err != nil { + http.Error(r, err.Error(), 500) + return + } + + // XML rendered fine, write out the result + r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset) + r.WriteHeader(status) + if len(r.Opt.PrefixXML) > 0 { + _, _ = r.Write(r.Opt.PrefixXML) + } + _, _ = r.Write(result) +} + +func (r *TplRender) data(status int, contentType string, v []byte) { + if r.Header().Get(_CONTENT_TYPE) == "" { + r.Header().Set(_CONTENT_TYPE, contentType) + } + r.WriteHeader(status) + _, _ = r.Write(v) +} + +func (r *TplRender) RawData(status int, v []byte) { + r.data(status, _CONTENT_BINARY, v) +} + +func (r *TplRender) PlainText(status int, v []byte) { + r.data(status, _CONTENT_PLAIN, v) +} + +func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) { + buf := bufpool.Get().(*bytes.Buffer) + return buf, t.ExecuteTemplate(buf, name, data) +} + +func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) { + funcs := template.FuncMap{ + "yield": func() (template.HTML, error) { + buf, err := r.execute(t, tplName, data) + // return safe html here since we are rendering our own template + return template.HTML(buf.String()), err + }, + "current": func() (string, error) { + return tplName, nil + }, + } + t.Funcs(funcs) +} + +func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) { + t := r.TemplateSet.Get(setName) + if Env == DEV { + opt := *r.Opt + opt.Directory = r.TemplateSet.GetDir(setName) + t = r.TemplateSet.Set(setName, &opt) + } + if t == nil { + return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName) + } + + opt := r.prepareHTMLOptions(htmlOpt) + + if len(opt.Layout) > 0 { + r.addYield(t, tplName, data) + tplName = opt.Layout + } + + out, err := r.execute(t, tplName, data) + if err != nil { + return nil, err + } + + return out, nil +} + +func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) { + r.startTime = time.Now() + + out, err := r.renderBytes(setName, tplName, data, htmlOpt...) + if err != nil { + http.Error(r, err.Error(), http.StatusInternalServerError) + return + } + + r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset) + r.WriteHeader(status) + + if _, err := out.WriteTo(r); err != nil { + out.Reset() + } + bufpool.Put(out) +} + +func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) { + r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...) +} + +func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) { + r.renderHTML(status, setName, tplName, data, htmlOpt...) +} + +func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) { + out, err := r.renderBytes(setName, tplName, data, htmlOpt...) + if err != nil { + return []byte(""), err + } + return out.Bytes(), nil +} + +func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) { + return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...) +} + +func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) { + p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...) + return string(p), err +} + +func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) { + p, err := r.HTMLBytes(name, data, htmlOpt...) + return string(p), err +} + +// Error writes the given HTTP status to the current ResponseWriter +func (r *TplRender) Error(status int, message ...string) { + r.WriteHeader(status) + if len(message) > 0 { + _, _ = r.Write([]byte(message[0])) + } +} + +func (r *TplRender) Status(status int) { + r.WriteHeader(status) +} + +func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions { + if len(htmlOpt) > 0 { + return htmlOpt[0] + } + + return HTMLOptions{ + Layout: r.Opt.Layout, + } +} + +func (r *TplRender) SetTemplatePath(setName, dir string) { + if len(setName) == 0 { + setName = DEFAULT_TPL_SET_NAME + } + opt := *r.Opt + opt.Directory = dir + r.TemplateSet.Set(setName, &opt) +} + +func (r *TplRender) HasTemplateSet(name string) bool { + return r.TemplateSet.Get(name) != nil +} + +// DummyRender is used when user does not choose any real render to use. +// This way, we can print out friendly message which asks them to register one, +// instead of ugly and confusing 'nil pointer' panic. +type DummyRender struct { + http.ResponseWriter +} + +func renderNotRegistered() { + panic("middleware render hasn't been registered") +} + +func (r *DummyRender) SetResponseWriter(http.ResponseWriter) { + renderNotRegistered() +} + +func (r *DummyRender) JSON(int, interface{}) { + renderNotRegistered() +} + +func (r *DummyRender) JSONString(interface{}) (string, error) { + renderNotRegistered() + return "", nil +} + +func (r *DummyRender) RawData(int, []byte) { + renderNotRegistered() +} + +func (r *DummyRender) PlainText(int, []byte) { + renderNotRegistered() +} + +func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) { + renderNotRegistered() +} + +func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) { + renderNotRegistered() +} + +func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) { + renderNotRegistered() + return "", nil +} + +func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) { + renderNotRegistered() + return "", nil +} + +func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) { + renderNotRegistered() + return nil, nil +} + +func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) { + renderNotRegistered() + return nil, nil +} + +func (r *DummyRender) XML(int, interface{}) { + renderNotRegistered() +} + +func (r *DummyRender) Error(int, ...string) { + renderNotRegistered() +} + +func (r *DummyRender) Status(int) { + renderNotRegistered() +} + +func (r *DummyRender) SetTemplatePath(string, string) { + renderNotRegistered() +} + +func (r *DummyRender) HasTemplateSet(string) bool { + renderNotRegistered() + return false +} diff --git a/pkg/macaron/response_writer.go b/pkg/macaron/response_writer.go new file mode 100644 index 00000000000..eeb35f642e6 --- /dev/null +++ b/pkg/macaron/response_writer.go @@ -0,0 +1,124 @@ +// Copyright 2013 Martini Authors +// +// 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 +// +// http://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 macaron + +import ( + "bufio" + "errors" + "net" + "net/http" +) + +// ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about +// the response. It is recommended that middleware handlers use this construct to wrap a responsewriter +// if the functionality calls for it. +type ResponseWriter interface { + http.ResponseWriter + http.Flusher + http.Pusher + // Status returns the status code of the response or 0 if the response has not been written. + Status() int + // Written returns whether or not the ResponseWriter has been written. + Written() bool + // Size returns the size of the response body. + Size() int + // Before allows for a function to be called before the ResponseWriter has been written to. This is + // useful for setting headers or any other operations that must happen before a response has been written. + Before(BeforeFunc) +} + +// BeforeFunc is a function that is called before the ResponseWriter has been written to. +type BeforeFunc func(ResponseWriter) + +// NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter +func NewResponseWriter(method string, rw http.ResponseWriter) ResponseWriter { + return &responseWriter{method, rw, 0, 0, nil} +} + +type responseWriter struct { + method string + http.ResponseWriter + status int + size int + beforeFuncs []BeforeFunc +} + +func (rw *responseWriter) WriteHeader(s int) { + rw.callBefore() + rw.ResponseWriter.WriteHeader(s) + rw.status = s +} + +func (rw *responseWriter) Write(b []byte) (size int, err error) { + if !rw.Written() { + // The status will be StatusOK if WriteHeader has not been called yet + rw.WriteHeader(http.StatusOK) + } + if rw.method != "HEAD" { + size, err = rw.ResponseWriter.Write(b) + rw.size += size + } + return size, err +} + +func (rw *responseWriter) Status() int { + return rw.status +} + +func (rw *responseWriter) Size() int { + return rw.size +} + +func (rw *responseWriter) Written() bool { + return rw.status != 0 +} + +func (rw *responseWriter) Before(before BeforeFunc) { + rw.beforeFuncs = append(rw.beforeFuncs, before) +} + +func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker, ok := rw.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") + } + return hijacker.Hijack() +} + +//nolint +func (rw *responseWriter) CloseNotify() <-chan bool { + return rw.ResponseWriter.(http.CloseNotifier).CloseNotify() +} + +func (rw *responseWriter) callBefore() { + for i := len(rw.beforeFuncs) - 1; i >= 0; i-- { + rw.beforeFuncs[i](rw) + } +} + +func (rw *responseWriter) Flush() { + flusher, ok := rw.ResponseWriter.(http.Flusher) + if ok { + flusher.Flush() + } +} + +func (rw *responseWriter) Push(target string, opts *http.PushOptions) error { + pusher, ok := rw.ResponseWriter.(http.Pusher) + if !ok { + return errors.New("the ResponseWriter doesn't support the Pusher interface") + } + return pusher.Push(target, opts) +} diff --git a/pkg/macaron/return_handler.go b/pkg/macaron/return_handler.go new file mode 100644 index 00000000000..51a1d5aa23b --- /dev/null +++ b/pkg/macaron/return_handler.go @@ -0,0 +1,74 @@ +// Copyright 2013 Martini Authors +// Copyright 2014 The Macaron Authors +// +// 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 +// +// http://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 macaron + +import ( + "net/http" + "reflect" +) + +// ReturnHandler is a service that Martini provides that is called +// when a route handler returns something. The ReturnHandler is +// responsible for writing to the ResponseWriter based on the values +// that are passed into this function. +type ReturnHandler func(*Context, []reflect.Value) + +func canDeref(val reflect.Value) bool { + return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr +} + +func isError(val reflect.Value) bool { + _, ok := val.Interface().(error) + return ok +} + +func isByteSlice(val reflect.Value) bool { + return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 +} + +func defaultReturnHandler() ReturnHandler { + return func(ctx *Context, vals []reflect.Value) { + rv := ctx.GetVal(InterfaceOf((*http.ResponseWriter)(nil))) + resp := rv.Interface().(http.ResponseWriter) + var respVal reflect.Value + if len(vals) > 1 && vals[0].Kind() == reflect.Int { + resp.WriteHeader(int(vals[0].Int())) + respVal = vals[1] + } else if len(vals) > 0 { + respVal = vals[0] + + if isError(respVal) { + err := respVal.Interface().(error) + if err != nil { + ctx.internalServerError(ctx, err) + } + return + } else if canDeref(respVal) { + if respVal.IsNil() { + return // Ignore nil error + } + } + } + if canDeref(respVal) { + respVal = respVal.Elem() + } + if isByteSlice(respVal) { + _, _ = resp.Write(respVal.Bytes()) + } else { + _, _ = resp.Write([]byte(respVal.String())) + } + } +} diff --git a/pkg/macaron/router.go b/pkg/macaron/router.go new file mode 100644 index 00000000000..df593d669ac --- /dev/null +++ b/pkg/macaron/router.go @@ -0,0 +1,380 @@ +// Copyright 2014 The Macaron Authors +// +// 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 +// +// http://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 macaron + +import ( + "net/http" + "strings" + "sync" +) + +var ( + // Known HTTP methods. + _HTTP_METHODS = map[string]bool{ + "GET": true, + "POST": true, + "PUT": true, + "DELETE": true, + "PATCH": true, + "OPTIONS": true, + "HEAD": true, + } +) + +// routeMap represents a thread-safe map for route tree. +type routeMap struct { + lock sync.RWMutex + routes map[string]map[string]*Leaf +} + +// NewRouteMap initializes and returns a new routeMap. +func NewRouteMap() *routeMap { + rm := &routeMap{ + routes: make(map[string]map[string]*Leaf), + } + for m := range _HTTP_METHODS { + rm.routes[m] = make(map[string]*Leaf) + } + return rm +} + +// getLeaf returns Leaf object if a route has been registered. +func (rm *routeMap) getLeaf(method, pattern string) *Leaf { + rm.lock.RLock() + defer rm.lock.RUnlock() + + return rm.routes[method][pattern] +} + +// add adds new route to route tree map. +func (rm *routeMap) add(method, pattern string, leaf *Leaf) { + rm.lock.Lock() + defer rm.lock.Unlock() + + rm.routes[method][pattern] = leaf +} + +type group struct { + pattern string + handlers []Handler +} + +// Router represents a Macaron router layer. +type Router struct { + m *Macaron + autoHead bool + routers map[string]*Tree + *routeMap + namedRoutes map[string]*Leaf + + groups []group + notFound http.HandlerFunc + internalServerError func(*Context, error) + + // handlerWrapper is used to wrap arbitrary function from Handler to inject.FastInvoker. + handlerWrapper func(Handler) Handler +} + +func NewRouter() *Router { + return &Router{ + routers: make(map[string]*Tree), + routeMap: NewRouteMap(), + namedRoutes: make(map[string]*Leaf), + } +} + +// SetAutoHead sets the value who determines whether add HEAD method automatically +// when GET method is added. +func (r *Router) SetAutoHead(v bool) { + r.autoHead = v +} + +type Params map[string]string + +// Handle is a function that can be registered to a route to handle HTTP requests. +// Like http.HandlerFunc, but has a third parameter for the values of wildcards (variables). +type Handle func(http.ResponseWriter, *http.Request, Params) + +// Route represents a wrapper of leaf route and upper level router. +type Route struct { + router *Router + leaf *Leaf +} + +// Name sets name of route. +func (r *Route) Name(name string) { + if len(name) == 0 { + panic("route name cannot be empty") + } else if r.router.namedRoutes[name] != nil { + panic("route with given name already exists: " + name) + } + r.router.namedRoutes[name] = r.leaf +} + +// handle adds new route to the router tree. +func (r *Router) handle(method, pattern string, handle Handle) *Route { + method = strings.ToUpper(method) + + var leaf *Leaf + // Prevent duplicate routes. + if leaf = r.getLeaf(method, pattern); leaf != nil { + return &Route{r, leaf} + } + + // Validate HTTP methods. + if !_HTTP_METHODS[method] && method != "*" { + panic("unknown HTTP method: " + method) + } + + // Generate methods need register. + methods := make(map[string]bool) + if method == "*" { + for m := range _HTTP_METHODS { + methods[m] = true + } + } else { + methods[method] = true + } + + // Add to router tree. + for m := range methods { + if t, ok := r.routers[m]; ok { + leaf = t.Add(pattern, handle) + } else { + t := NewTree() + leaf = t.Add(pattern, handle) + r.routers[m] = t + } + r.add(m, pattern, leaf) + } + return &Route{r, leaf} +} + +// Handle registers a new request handle with the given pattern, method and handlers. +func (r *Router) Handle(method string, pattern string, handlers []Handler) *Route { + if len(r.groups) > 0 { + groupPattern := "" + h := make([]Handler, 0) + for _, g := range r.groups { + groupPattern += g.pattern + h = append(h, g.handlers...) + } + + pattern = groupPattern + pattern + h = append(h, handlers...) + handlers = h + } + handlers = validateAndWrapHandlers(handlers, r.handlerWrapper) + + return r.handle(method, pattern, func(resp http.ResponseWriter, req *http.Request, params Params) { + c := r.m.createContext(resp, req) + c.params = params + c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers)) + c.handlers = append(c.handlers, r.m.handlers...) + c.handlers = append(c.handlers, handlers...) + c.run() + }) +} + +func (r *Router) Group(pattern string, fn func(), h ...Handler) { + r.groups = append(r.groups, group{pattern, h}) + fn() + r.groups = r.groups[:len(r.groups)-1] +} + +// Get is a shortcut for r.Handle("GET", pattern, handlers) +func (r *Router) Get(pattern string, h ...Handler) (leaf *Route) { + leaf = r.Handle("GET", pattern, h) + if r.autoHead { + r.Head(pattern, h...) + } + return leaf +} + +// Patch is a shortcut for r.Handle("PATCH", pattern, handlers) +func (r *Router) Patch(pattern string, h ...Handler) *Route { + return r.Handle("PATCH", pattern, h) +} + +// Post is a shortcut for r.Handle("POST", pattern, handlers) +func (r *Router) Post(pattern string, h ...Handler) *Route { + return r.Handle("POST", pattern, h) +} + +// Put is a shortcut for r.Handle("PUT", pattern, handlers) +func (r *Router) Put(pattern string, h ...Handler) *Route { + return r.Handle("PUT", pattern, h) +} + +// Delete is a shortcut for r.Handle("DELETE", pattern, handlers) +func (r *Router) Delete(pattern string, h ...Handler) *Route { + return r.Handle("DELETE", pattern, h) +} + +// Options is a shortcut for r.Handle("OPTIONS", pattern, handlers) +func (r *Router) Options(pattern string, h ...Handler) *Route { + return r.Handle("OPTIONS", pattern, h) +} + +// Head is a shortcut for r.Handle("HEAD", pattern, handlers) +func (r *Router) Head(pattern string, h ...Handler) *Route { + return r.Handle("HEAD", pattern, h) +} + +// Any is a shortcut for r.Handle("*", pattern, handlers) +func (r *Router) Any(pattern string, h ...Handler) *Route { + return r.Handle("*", pattern, h) +} + +// Route is a shortcut for same handlers but different HTTP methods. +// +// Example: +// m.Route("/", "GET,POST", h) +func (r *Router) Route(pattern, methods string, h ...Handler) (route *Route) { + for _, m := range strings.Split(methods, ",") { + route = r.Handle(strings.TrimSpace(m), pattern, h) + } + return route +} + +// Combo returns a combo router. +func (r *Router) Combo(pattern string, h ...Handler) *ComboRouter { + return &ComboRouter{r, pattern, h, map[string]bool{}, nil} +} + +// NotFound configurates http.HandlerFunc which is called when no matching route is +// found. If it is not set, http.NotFound is used. +// Be sure to set 404 response code in your handler. +func (r *Router) NotFound(handlers ...Handler) { + handlers = validateAndWrapHandlers(handlers) + r.notFound = func(rw http.ResponseWriter, req *http.Request) { + c := r.m.createContext(rw, req) + c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers)) + c.handlers = append(c.handlers, r.m.handlers...) + c.handlers = append(c.handlers, handlers...) + c.run() + } +} + +// InternalServerError configurates handler which is called when route handler returns +// error. If it is not set, default handler is used. +// Be sure to set 500 response code in your handler. +func (r *Router) InternalServerError(handlers ...Handler) { + handlers = validateAndWrapHandlers(handlers) + r.internalServerError = func(c *Context, err error) { + c.index = 0 + c.handlers = handlers + c.Map(err) + c.run() + } +} + +// SetHandlerWrapper sets handlerWrapper for the router. +func (r *Router) SetHandlerWrapper(f func(Handler) Handler) { + r.handlerWrapper = f +} + +func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if t, ok := r.routers[req.Method]; ok { + // Fast match for static routes + leaf := r.getLeaf(req.Method, req.URL.Path) + if leaf != nil { + leaf.handle(rw, req, nil) + return + } + + h, p, ok := t.Match(req.URL.EscapedPath()) + if ok { + if splat, ok := p["*0"]; ok { + p["*"] = splat // Easy name. + } + h(rw, req, p) + return + } + } + + r.notFound(rw, req) +} + +// URLFor builds path part of URL by given pair values. +func (r *Router) URLFor(name string, pairs ...string) string { + leaf, ok := r.namedRoutes[name] + if !ok { + panic("route with given name does not exists: " + name) + } + return leaf.URLPath(pairs...) +} + +// ComboRouter represents a combo router. +type ComboRouter struct { + router *Router + pattern string + handlers []Handler + methods map[string]bool // Registered methods. + + lastRoute *Route +} + +func (cr *ComboRouter) checkMethod(name string) { + if cr.methods[name] { + panic("method '" + name + "' has already been registered") + } + cr.methods[name] = true +} + +func (cr *ComboRouter) route(fn func(string, ...Handler) *Route, method string, h ...Handler) *ComboRouter { + cr.checkMethod(method) + cr.lastRoute = fn(cr.pattern, append(cr.handlers, h...)...) + return cr +} + +func (cr *ComboRouter) Get(h ...Handler) *ComboRouter { + if cr.router.autoHead { + cr.Head(h...) + } + return cr.route(cr.router.Get, "GET", h...) +} + +func (cr *ComboRouter) Patch(h ...Handler) *ComboRouter { + return cr.route(cr.router.Patch, "PATCH", h...) +} + +func (cr *ComboRouter) Post(h ...Handler) *ComboRouter { + return cr.route(cr.router.Post, "POST", h...) +} + +func (cr *ComboRouter) Put(h ...Handler) *ComboRouter { + return cr.route(cr.router.Put, "PUT", h...) +} + +func (cr *ComboRouter) Delete(h ...Handler) *ComboRouter { + return cr.route(cr.router.Delete, "DELETE", h...) +} + +func (cr *ComboRouter) Options(h ...Handler) *ComboRouter { + return cr.route(cr.router.Options, "OPTIONS", h...) +} + +func (cr *ComboRouter) Head(h ...Handler) *ComboRouter { + return cr.route(cr.router.Head, "HEAD", h...) +} + +// Name sets name of ComboRouter route. +func (cr *ComboRouter) Name(name string) { + if cr.lastRoute == nil { + panic("no corresponding route to be named") + } + cr.lastRoute.Name(name) +} diff --git a/pkg/macaron/tree.go b/pkg/macaron/tree.go new file mode 100644 index 00000000000..9fefcb0c4f5 --- /dev/null +++ b/pkg/macaron/tree.go @@ -0,0 +1,390 @@ +// Copyright 2015 The Macaron Authors +// +// 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 +// +// http://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 macaron + +import ( + urlpkg "net/url" + "regexp" + "strconv" + "strings" +) + +type patternType int8 + +const ( + _PATTERN_STATIC patternType = iota // /home + _PATTERN_REGEXP // /:id([0-9]+) + _PATTERN_PATH_EXT // /*.* + _PATTERN_HOLDER // /:user + _PATTERN_MATCH_ALL // /* +) + +// Leaf represents a leaf route information. +type Leaf struct { + parent *Tree + + typ patternType + pattern string + rawPattern string // Contains wildcard instead of regexp + wildcards []string + reg *regexp.Regexp + optional bool + + handle Handle +} + +var wildcardPattern = regexp.MustCompile(`:[a-zA-Z0-9]+`) + +func isSpecialRegexp(pattern, regStr string, pos []int) bool { + return len(pattern) >= pos[1]+len(regStr) && pattern[pos[1]:pos[1]+len(regStr)] == regStr +} + +// getNextWildcard tries to find next wildcard and update pattern with corresponding regexp. +func getNextWildcard(pattern string) (wildcard, _ string) { + pos := wildcardPattern.FindStringIndex(pattern) + if pos == nil { + return "", pattern + } + wildcard = pattern[pos[0]:pos[1]] + + // Reach last character or no regexp is given. + if len(pattern) == pos[1] { + return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1) + } else if pattern[pos[1]] != '(' { + switch { + case isSpecialRegexp(pattern, ":int", pos): + pattern = strings.Replace(pattern, ":int", "([0-9]+)", 1) + case isSpecialRegexp(pattern, ":string", pos): + pattern = strings.Replace(pattern, ":string", "([\\w]+)", 1) + default: + return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1) + } + } + + // Cut out placeholder directly. + return wildcard, pattern[:pos[0]] + pattern[pos[1]:] +} + +func getWildcards(pattern string) (string, []string) { + wildcards := make([]string, 0, 2) + + // Keep getting next wildcard until nothing is left. + var wildcard string + for { + wildcard, pattern = getNextWildcard(pattern) + if len(wildcard) > 0 { + wildcards = append(wildcards, wildcard) + } else { + break + } + } + + return pattern, wildcards +} + +// getRawPattern removes all regexp but keeps wildcards for building URL path. +func getRawPattern(rawPattern string) string { + rawPattern = strings.Replace(rawPattern, ":int", "", -1) + rawPattern = strings.Replace(rawPattern, ":string", "", -1) + + for { + startIdx := strings.Index(rawPattern, "(") + if startIdx == -1 { + break + } + + closeIdx := strings.Index(rawPattern, ")") + if closeIdx > -1 { + rawPattern = rawPattern[:startIdx] + rawPattern[closeIdx+1:] + } + } + return rawPattern +} + +func checkPattern(pattern string) (typ patternType, rawPattern string, wildcards []string, reg *regexp.Regexp) { + pattern = strings.TrimLeft(pattern, "?") + rawPattern = getRawPattern(pattern) + + if pattern == "*" { + typ = _PATTERN_MATCH_ALL + } else if pattern == "*.*" { + typ = _PATTERN_PATH_EXT + } else if strings.Contains(pattern, ":") { + typ = _PATTERN_REGEXP + pattern, wildcards = getWildcards(pattern) + if pattern == "(.+)" { + typ = _PATTERN_HOLDER + } else { + reg = regexp.MustCompile(pattern) + } + } + return typ, rawPattern, wildcards, reg +} + +func NewLeaf(parent *Tree, pattern string, handle Handle) *Leaf { + typ, rawPattern, wildcards, reg := checkPattern(pattern) + optional := false + if len(pattern) > 0 && pattern[0] == '?' { + optional = true + } + return &Leaf{parent, typ, pattern, rawPattern, wildcards, reg, optional, handle} +} + +// URLPath build path part of URL by given pair values. +func (l *Leaf) URLPath(pairs ...string) string { + if len(pairs)%2 != 0 { + panic("number of pairs does not match") + } + + urlPath := l.rawPattern + parent := l.parent + for parent != nil { + urlPath = parent.rawPattern + "/" + urlPath + parent = parent.parent + } + for i := 0; i < len(pairs); i += 2 { + if len(pairs[i]) == 0 { + panic("pair value cannot be empty: " + strconv.Itoa(i)) + } else if pairs[i][0] != ':' && pairs[i] != "*" && pairs[i] != "*.*" { + pairs[i] = ":" + pairs[i] + } + urlPath = strings.Replace(urlPath, pairs[i], pairs[i+1], 1) + } + return urlPath +} + +// Tree represents a router tree in Macaron. +type Tree struct { + parent *Tree + + typ patternType + pattern string + rawPattern string + wildcards []string + reg *regexp.Regexp + + subtrees []*Tree + leaves []*Leaf +} + +func NewSubtree(parent *Tree, pattern string) *Tree { + typ, rawPattern, wildcards, reg := checkPattern(pattern) + return &Tree{parent, typ, pattern, rawPattern, wildcards, reg, make([]*Tree, 0, 5), make([]*Leaf, 0, 5)} +} + +func NewTree() *Tree { + return NewSubtree(nil, "") +} + +func (t *Tree) addLeaf(pattern string, handle Handle) *Leaf { + for i := 0; i < len(t.leaves); i++ { + if t.leaves[i].pattern == pattern { + return t.leaves[i] + } + } + + leaf := NewLeaf(t, pattern, handle) + + // Add exact same leaf to grandparent/parent level without optional. + if leaf.optional { + parent := leaf.parent + if parent.parent != nil { + parent.parent.addLeaf(parent.pattern, handle) + } else { + parent.addLeaf("", handle) // Root tree can add as empty pattern. + } + } + + i := 0 + for ; i < len(t.leaves); i++ { + if leaf.typ < t.leaves[i].typ { + break + } + } + + if i == len(t.leaves) { + t.leaves = append(t.leaves, leaf) + } else { + t.leaves = append(t.leaves[:i], append([]*Leaf{leaf}, t.leaves[i:]...)...) + } + return leaf +} + +func (t *Tree) addSubtree(segment, pattern string, handle Handle) *Leaf { + for i := 0; i < len(t.subtrees); i++ { + if t.subtrees[i].pattern == segment { + return t.subtrees[i].addNextSegment(pattern, handle) + } + } + + subtree := NewSubtree(t, segment) + i := 0 + for ; i < len(t.subtrees); i++ { + if subtree.typ < t.subtrees[i].typ { + break + } + } + + if i == len(t.subtrees) { + t.subtrees = append(t.subtrees, subtree) + } else { + t.subtrees = append(t.subtrees[:i], append([]*Tree{subtree}, t.subtrees[i:]...)...) + } + return subtree.addNextSegment(pattern, handle) +} + +func (t *Tree) addNextSegment(pattern string, handle Handle) *Leaf { + pattern = strings.TrimPrefix(pattern, "/") + + i := strings.Index(pattern, "/") + if i == -1 { + return t.addLeaf(pattern, handle) + } + return t.addSubtree(pattern[:i], pattern[i+1:], handle) +} + +func (t *Tree) Add(pattern string, handle Handle) *Leaf { + pattern = strings.TrimSuffix(pattern, "/") + return t.addNextSegment(pattern, handle) +} + +func (t *Tree) matchLeaf(globLevel int, url string, params Params) (Handle, bool) { + url, err := urlpkg.PathUnescape(url) + if err != nil { + return nil, false + } + for i := 0; i < len(t.leaves); i++ { + switch t.leaves[i].typ { + case _PATTERN_STATIC: + if t.leaves[i].pattern == url { + return t.leaves[i].handle, true + } + case _PATTERN_REGEXP: + results := t.leaves[i].reg.FindStringSubmatch(url) + // Number of results and wildcasrd should be exact same. + if len(results)-1 != len(t.leaves[i].wildcards) { + break + } + + for j := 0; j < len(t.leaves[i].wildcards); j++ { + params[t.leaves[i].wildcards[j]] = results[j+1] + } + return t.leaves[i].handle, true + case _PATTERN_PATH_EXT: + j := strings.LastIndex(url, ".") + if j > -1 { + params[":path"] = url[:j] + params[":ext"] = url[j+1:] + } else { + params[":path"] = url + } + return t.leaves[i].handle, true + case _PATTERN_HOLDER: + params[t.leaves[i].wildcards[0]] = url + return t.leaves[i].handle, true + case _PATTERN_MATCH_ALL: + params["*"] = url + params["*"+strconv.Itoa(globLevel)] = url + return t.leaves[i].handle, true + } + } + return nil, false +} + +func (t *Tree) matchSubtree(globLevel int, segment, url string, params Params) (Handle, bool) { + unescapedSegment, err := urlpkg.PathUnescape(segment) + if err != nil { + return nil, false + } + for i := 0; i < len(t.subtrees); i++ { + switch t.subtrees[i].typ { + case _PATTERN_STATIC: + if t.subtrees[i].pattern == unescapedSegment { + if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok { + return handle, true + } + } + case _PATTERN_REGEXP: + results := t.subtrees[i].reg.FindStringSubmatch(unescapedSegment) + if len(results)-1 != len(t.subtrees[i].wildcards) { + break + } + + for j := 0; j < len(t.subtrees[i].wildcards); j++ { + params[t.subtrees[i].wildcards[j]] = results[j+1] + } + if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok { + return handle, true + } + case _PATTERN_HOLDER: + if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok { + params[t.subtrees[i].wildcards[0]] = unescapedSegment + return handle, true + } + case _PATTERN_MATCH_ALL: + if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok { + params["*"+strconv.Itoa(globLevel)] = unescapedSegment + return handle, true + } + } + } + + if len(t.leaves) > 0 { + leaf := t.leaves[len(t.leaves)-1] + unescapedURL, err := urlpkg.PathUnescape(segment + "/" + url) + if err != nil { + return nil, false + } + if leaf.typ == _PATTERN_PATH_EXT { + j := strings.LastIndex(unescapedURL, ".") + if j > -1 { + params[":path"] = unescapedURL[:j] + params[":ext"] = unescapedURL[j+1:] + } else { + params[":path"] = unescapedURL + } + return leaf.handle, true + } else if leaf.typ == _PATTERN_MATCH_ALL { + params["*"] = unescapedURL + params["*"+strconv.Itoa(globLevel)] = unescapedURL + return leaf.handle, true + } + } + return nil, false +} + +func (t *Tree) matchNextSegment(globLevel int, url string, params Params) (Handle, bool) { + i := strings.Index(url, "/") + if i == -1 { + return t.matchLeaf(globLevel, url, params) + } + return t.matchSubtree(globLevel, url[:i], url[i+1:], params) +} + +func (t *Tree) Match(url string) (Handle, Params, bool) { + url = strings.TrimPrefix(url, "/") + url = strings.TrimSuffix(url, "/") + params := make(Params) + handle, ok := t.matchNextSegment(0, url, params) + return handle, params, ok +} + +// MatchTest returns true if given URL is matched by given pattern. +func MatchTest(pattern, url string) bool { + t := NewTree() + t.Add(pattern, nil) + _, _, ok := t.Match(url) + return ok +} diff --git a/pkg/services/live/pushhttp/push.go b/pkg/services/live/pushhttp/push.go index e6df5471186..59ee360757d 100644 --- a/pkg/services/live/pushhttp/push.go +++ b/pkg/services/live/pushhttp/push.go @@ -3,6 +3,7 @@ package pushhttp import ( "context" "errors" + "io" "net/http" "github.com/grafana/grafana/pkg/infra/log" @@ -58,7 +59,7 @@ func (g *Gateway) Handle(ctx *models.ReqContext) { urlValues := ctx.Req.URL.Query() frameFormat := pushurl.FrameFormatFromValues(urlValues) - body, err := ctx.Req.Body().Bytes() + body, err := io.ReadAll(ctx.Req.Request.Body) if err != nil { logger.Error("Error reading body", "error", err) ctx.Resp.WriteHeader(http.StatusInternalServerError)