/* Package echo implements high performance, minimalist Go web framework. Example: package main import ( "net/http" "github.com/labstack/echo" "github.com/labstack/echo/middleware" ) // Handler func hello(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") } func main() { // Echo instance e := echo.New() // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) // Routes e.GET("/", hello) // Start server e.Logger.Fatal(e.Start(":1323")) } Learn more at https://echo.labstack.com */ package echo import ( "bytes" "crypto/tls" "errors" "fmt" "io" slog "log" "net/http" "path" "reflect" "runtime" "sync" "time" "github.com/labstack/gommon/color" "github.com/labstack/gommon/log" "github.com/tylerb/graceful" "golang.org/x/crypto/acme/autocert" ) type ( // Echo is the top-level framework instance. Echo struct { DisableHTTP2 bool Debug bool HTTPErrorHandler HTTPErrorHandler Binder Binder Validator Validator Renderer Renderer AutoTLSManager autocert.Manager ReadTimeout time.Duration WriteTimeout time.Duration ShutdownTimeout time.Duration Color *color.Color Logger Logger stdLogger *slog.Logger server *graceful.Server tlsServer *graceful.Server premiddleware []MiddlewareFunc middleware []MiddlewareFunc maxParam *int router *Router notFoundHandler HandlerFunc pool sync.Pool } // Route contains a handler and information for matching against requests. Route struct { Method string Path string Handler string } // HTTPError represents an error that occurred while handling a request. HTTPError struct { Code int Message interface{} } // MiddlewareFunc defines a function to process middleware. MiddlewareFunc func(HandlerFunc) HandlerFunc // HandlerFunc defines a function to server HTTP requests. HandlerFunc func(Context) error // HTTPErrorHandler is a centralized HTTP error handler. HTTPErrorHandler func(error, Context) // Validator is the interface that wraps the Validate function. Validator interface { Validate(i interface{}) error } // Renderer is the interface that wraps the Render function. Renderer interface { Render(io.Writer, string, interface{}, Context) error } // Map defines a generic map of type `map[string]interface{}`. Map map[string]interface{} // i is the interface for Echo and Group. i interface { GET(string, HandlerFunc, ...MiddlewareFunc) } ) // HTTP methods const ( CONNECT = "CONNECT" DELETE = "DELETE" GET = "GET" HEAD = "HEAD" OPTIONS = "OPTIONS" PATCH = "PATCH" POST = "POST" PUT = "PUT" TRACE = "TRACE" ) // MIME types const ( MIMEApplicationJSON = "application/json" MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8 MIMEApplicationJavaScript = "application/javascript" MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 MIMEApplicationXML = "application/xml" MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + charsetUTF8 MIMEApplicationForm = "application/x-www-form-urlencoded" MIMEApplicationProtobuf = "application/protobuf" MIMEApplicationMsgpack = "application/msgpack" MIMETextHTML = "text/html" MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8 MIMETextPlain = "text/plain" MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8 MIMEMultipartForm = "multipart/form-data" MIMEOctetStream = "application/octet-stream" ) const ( charsetUTF8 = "charset=UTF-8" ) // Headers const ( HeaderAcceptEncoding = "Accept-Encoding" HeaderAllow = "Allow" HeaderAuthorization = "Authorization" HeaderContentDisposition = "Content-Disposition" HeaderContentEncoding = "Content-Encoding" HeaderContentLength = "Content-Length" HeaderContentType = "Content-Type" HeaderCookie = "Cookie" HeaderSetCookie = "Set-Cookie" HeaderIfModifiedSince = "If-Modified-Since" HeaderLastModified = "Last-Modified" HeaderLocation = "Location" HeaderUpgrade = "Upgrade" HeaderVary = "Vary" HeaderWWWAuthenticate = "WWW-Authenticate" HeaderXForwardedProto = "X-Forwarded-Proto" HeaderXHTTPMethodOverride = "X-HTTP-Method-Override" HeaderXForwardedFor = "X-Forwarded-For" HeaderXRealIP = "X-Real-IP" HeaderServer = "Server" HeaderOrigin = "Origin" HeaderAccessControlRequestMethod = "Access-Control-Request-Method" HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" HeaderAccessControlMaxAge = "Access-Control-Max-Age" // Security HeaderStrictTransportSecurity = "Strict-Transport-Security" HeaderXContentTypeOptions = "X-Content-Type-Options" HeaderXXSSProtection = "X-XSS-Protection" HeaderXFrameOptions = "X-Frame-Options" HeaderContentSecurityPolicy = "Content-Security-Policy" HeaderXCSRFToken = "X-CSRF-Token" ) var ( methods = [...]string{ CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, } ) // Errors var ( ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) ErrNotFound = NewHTTPError(http.StatusNotFound) ErrUnauthorized = NewHTTPError(http.StatusUnauthorized) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) ErrValidatorNotRegistered = errors.New("Validator not registered") ErrRendererNotRegistered = errors.New("Renderer not registered") ErrInvalidRedirectCode = errors.New("Invalid redirect status code") ErrCookieNotFound = errors.New("Cookie not found") ) // Error handlers var ( NotFoundHandler = func(c Context) error { return ErrNotFound } MethodNotAllowedHandler = func(c Context) error { return ErrMethodNotAllowed } ) // New creates an instance of Echo. func New() (e *Echo) { e = &Echo{ AutoTLSManager: autocert.Manager{ Prompt: autocert.AcceptTOS, }, ShutdownTimeout: 15 * time.Second, Logger: log.New("echo"), maxParam: new(int), Color: color.New(), } e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.Binder = &DefaultBinder{} e.Logger.SetLevel(log.OFF) e.stdLogger = slog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.pool.New = func() interface{} { return e.NewContext(nil, nil) } e.router = NewRouter(e) return } // NewContext returns a Context instance. func (e *Echo) NewContext(r *http.Request, w http.ResponseWriter) Context { return &context{ request: r, response: NewResponse(w, e), store: make(Map), echo: e, pvalues: make([]string, *e.maxParam), handler: NotFoundHandler, } } // Router returns router. func (e *Echo) Router() *Router { return e.router } // DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response // with status code. func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { var ( code = http.StatusInternalServerError msg interface{} ) if he, ok := err.(*HTTPError); ok { code = he.Code msg = he.Message } else { msg = http.StatusText(code) } if _, ok := msg.(string); ok { msg = Map{"message": msg} } if !c.Response().Committed { if c.Request().Method == HEAD { // Issue #608 if err := c.NoContent(code); err != nil { goto ERROR } } else { if err := c.JSON(code, msg); err != nil { goto ERROR } } } ERROR: e.Logger.Error(err) } // Pre adds middleware to the chain which is run before router. func (e *Echo) Pre(middleware ...MiddlewareFunc) { e.premiddleware = append(e.premiddleware, middleware...) } // Use adds middleware to the chain which is run after router. func (e *Echo) Use(middleware ...MiddlewareFunc) { e.middleware = append(e.middleware, middleware...) } // CONNECT registers a new CONNECT route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(CONNECT, path, h, m...) } // DELETE registers a new DELETE route for a path with matching handler in the router // with optional route-level middleware. func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(DELETE, path, h, m...) } // GET registers a new GET route for a path with matching handler in the router // with optional route-level middleware. func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(GET, path, h, m...) } // HEAD registers a new HEAD route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(HEAD, path, h, m...) } // OPTIONS registers a new OPTIONS route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(OPTIONS, path, h, m...) } // PATCH registers a new PATCH route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(PATCH, path, h, m...) } // POST registers a new POST route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(POST, path, h, m...) } // PUT registers a new PUT route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(PUT, path, h, m...) } // TRACE registers a new TRACE route for a path with matching handler in the // router with optional route-level middleware. func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { e.add(TRACE, path, h, m...) } // Any registers a new route for all HTTP methods and path with matching handler // in the router with optional route-level middleware. func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { for _, m := range methods { e.add(m, path, handler, middleware...) } } // Match registers a new route for multiple HTTP methods and path with matching // handler in the router with optional route-level middleware. func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { for _, m := range methods { e.add(m, path, handler, middleware...) } } // Static registers a new route with path prefix to serve static files from the // provided root directory. func (e *Echo) Static(prefix, root string) { static(e, prefix, root) } func static(i i, prefix, root string) { h := func(c Context) error { return c.File(path.Join(root, c.Param("*"))) } i.GET(prefix, h) if prefix == "/" { i.GET(prefix+"*", h) } else { i.GET(prefix+"/*", h) } } // File registers a new route with path to serve a static file. func (e *Echo) File(path, file string) { e.GET(path, func(c Context) error { return c.File(file) }) } func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { name := handlerName(handler) e.router.Add(method, path, func(c Context) error { h := handler // Chain middleware for i := len(middleware) - 1; i >= 0; i-- { h = middleware[i](h) } return h(c) }) r := Route{ Method: method, Path: path, Handler: name, } e.router.routes[method+path] = r } // Group creates a new router group with prefix and optional group-level middleware. func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) { g = &Group{prefix: prefix, echo: e} g.Use(m...) return } // URI generates a URI from handler. func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { uri := new(bytes.Buffer) ln := len(params) n := 0 name := handlerName(handler) for _, r := range e.router.routes { if r.Handler == name { for i, l := 0, len(r.Path); i < l; i++ { if r.Path[i] == ':' && n < ln { for ; i < l && r.Path[i] != '/'; i++ { } uri.WriteString(fmt.Sprintf("%v", params[n])) n++ } if i < l { uri.WriteByte(r.Path[i]) } } break } } return uri.String() } // URL is an alias for `URI` function. func (e *Echo) URL(h HandlerFunc, params ...interface{}) string { return e.URI(h, params...) } // Routes returns the registered routes. func (e *Echo) Routes() []Route { routes := []Route{} for _, v := range e.router.routes { routes = append(routes, v) } return routes } // AcquireContext returns an empty `Context` instance from the pool. // You must return the context by calling `ReleaseContext()`. func (e *Echo) AcquireContext() Context { return e.pool.Get().(Context) } // ReleaseContext returns the `Context` instance back to the pool. // You must call it after `AcquireContext()`. func (e *Echo) ReleaseContext(c Context) { e.pool.Put(c) } // ServeHTTP implements `http.Handler` interface, which serves HTTP requests. func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := e.pool.Get().(*context) c.Reset(r, w) // Middleware h := func(c Context) error { method := r.Method path := r.URL.EscapedPath() e.router.Find(method, path, c) h := c.Handler() for i := len(e.middleware) - 1; i >= 0; i-- { h = e.middleware[i](h) } return h(c) } // Premiddleware for i := len(e.premiddleware) - 1; i >= 0; i-- { h = e.premiddleware[i](h) } // Execute chain if err := h(c); err != nil { e.HTTPErrorHandler(err, c) } e.pool.Put(c) } // Start starts the HTTP server. func (e *Echo) Start(address string) error { return e.StartServer(&http.Server{ Addr: address, ReadTimeout: e.ReadTimeout, WriteTimeout: e.WriteTimeout, ErrorLog: e.stdLogger, }) } // StartTLS starts the HTTPS server. func (e *Echo) StartTLS(address string, certFile, keyFile string) (err error) { if certFile == "" || keyFile == "" { return errors.New("invalid tls configuration") } config := new(tls.Config) config.Certificates = make([]tls.Certificate, 1) config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return } return e.startTLS(address, config) } // StartAutoTLS starts the HTTPS server using certificates automatically from https://letsencrypt.org. func (e *Echo) StartAutoTLS(address string) error { config := new(tls.Config) config.GetCertificate = e.AutoTLSManager.GetCertificate return e.startTLS(address, config) } func (e *Echo) startTLS(address string, config *tls.Config) error { if !e.DisableHTTP2 { config.NextProtos = append(config.NextProtos, "h2") } return e.StartServer(&http.Server{ Addr: address, ReadTimeout: e.ReadTimeout, WriteTimeout: e.WriteTimeout, TLSConfig: config, ErrorLog: e.stdLogger, }) } // StartServer runs a custom HTTP server. func (e *Echo) StartServer(s *http.Server) error { s.Handler = e gs := &graceful.Server{ Server: s, Timeout: e.ShutdownTimeout, Logger: e.stdLogger, } if s.TLSConfig == nil { e.server = gs e.Color.Printf(" ⇛ http server started on %s\n", color.Green(s.Addr)) return gs.ListenAndServe() } e.tlsServer = gs e.Color.Printf(" ⇛ https server started on %s\n", color.Green(s.Addr)) return gs.ListenAndServeTLSConfig(s.TLSConfig) } // Shutdown gracefully shutdown the HTTP server with timeout. func (e *Echo) Shutdown(timeout time.Duration) { e.server.Stop(timeout) } // ShutdownTLS gracefully shutdown the TLS server with timeout. func (e *Echo) ShutdownTLS(timeout time.Duration) { e.tlsServer.Stop(timeout) } // NewHTTPError creates a new HTTPError instance. func NewHTTPError(code int, message ...interface{}) *HTTPError { he := &HTTPError{Code: code, Message: http.StatusText(code)} if len(message) > 0 { he.Message = message[0] } return he } // Error makes it compatible with `error` interface. func (he *HTTPError) Error() string { return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message) } // WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. func WrapHandler(h http.Handler) HandlerFunc { return func(c Context) error { h.ServeHTTP(c.Response(), c.Request()) return nil } } // WrapMiddleware wraps `func(http.Handler) http.Handler` into `echo.MiddlewareFunc` func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc { return func(next HandlerFunc) HandlerFunc { return func(c Context) (err error) { m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.SetRequest(r) err = next(c) })).ServeHTTP(c.Response(), c.Request()) return } } } func handlerName(h HandlerFunc) string { t := reflect.ValueOf(h).Type() if t.Kind() == reflect.Func { return runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() } return t.String() }