func (s *Server) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
	router := s.getRouter(path)
	router.Path(path).Methods(http.MethodGet).Handler(s.echo)
	return s.echo.GET(replaceEchoVars(path), h, m...)
}
GET registers a GET handler at path.
package web
import (
	"context"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"github.com/gorilla/csrf"
	"github.com/gorilla/mux"
	echo "github.com/labstack/echo/v4"
	echomiddleware "github.com/labstack/echo/v4/middleware"
	"go.thethings.network/lorawan-stack/v3/pkg/errors"
	"go.thethings.network/lorawan-stack/v3/pkg/fillcontext"
	"go.thethings.network/lorawan-stack/v3/pkg/log"
	"go.thethings.network/lorawan-stack/v3/pkg/random"
	"go.thethings.network/lorawan-stack/v3/pkg/webhandlers"
	"go.thethings.network/lorawan-stack/v3/pkg/webmiddleware"
	"go.thethings.network/lorawan-stack/v3/pkg/webui"
	"gopkg.in/yaml.v2"
)
type Registerer interface {
	RegisterRoutes(s *Server)
}
type Server struct {
	root *mux.Router
	router *mux.Router
	apiRouter *mux.Router
	echo *echo.Echo
}
type options struct {
	cookieHashKey  []byte
	cookieBlockKey []byte
	staticMount       string
	staticSearchPaths []string
	trustedProxies []string
	contextFillers []fillcontext.Filler
	redirectToHost  string
	redirectToHTTPS map[int]int
	logIgnorePaths []string
}
type Option func(*options)
func WithContextFiller(contextFillers ...fillcontext.Filler) Option {
	return func(o *options) {
		o.contextFillers = append(o.contextFillers, contextFillers...)
	}
}
func WithTrustedProxies(cidrs ...string) Option {
	return func(o *options) {
		o.trustedProxies = append(o.trustedProxies, cidrs...)
	}
}
func WithCookieKeys(hashKey, blockKey []byte) Option {
	return func(o *options) {
		o.cookieHashKey, o.cookieBlockKey = hashKey, blockKey
	}
}
func WithStatic(mount string, searchPaths ...string) Option {
	return func(o *options) {
		o.staticMount, o.staticSearchPaths = mount, searchPaths
	}
}
func WithRedirectToHost(target string) Option {
	return func(o *options) {
		o.redirectToHost = target
	}
}
func WithRedirectToHTTPS(from, to int) Option {
	return func(o *options) {
		if o.redirectToHTTPS == nil {
			o.redirectToHTTPS = make(map[int]int)
		}
		o.redirectToHTTPS[from] = to
	}
}
func WithLogIgnorePaths(paths []string) Option {
	return func(o *options) {
		o.logIgnorePaths = paths
	}
}
func New(ctx context.Context, opts ...Option) (*Server, error) {
	logger := log.FromContext(ctx).WithField("namespace", "web")
	options := new(options)
	for _, opt := range opts {
		opt(options)
	}
	hashKey, blockKey := options.cookieHashKey, options.cookieBlockKey
	if len(hashKey) == 0 || isZeros(hashKey) {
		hashKey = random.Bytes(64)
		logger.Warn("No cookie hash key configured, generated a random one")
	}
	if len(hashKey) != 32 && len(hashKey) != 64 {
		return nil, errors.New("Expected cookie hash key to be 32 or 64 bytes long")
	}
	if len(blockKey) == 0 || isZeros(blockKey) {
		blockKey = random.Bytes(32)
		logger.Warn("No cookie block key configured, generated a random one")
	}
	if len(blockKey) != 32 {
		return nil, errors.New("Expected cookie block key to be 32 bytes long")
	}
	var proxyConfiguration webmiddleware.ProxyConfiguration
	proxyConfiguration.ParseAndAddTrusted(options.trustedProxies...)
	root := mux.NewRouter()
	root.NotFoundHandler = http.HandlerFunc(webhandlers.NotFound)
	root.Use(
		mux.MiddlewareFunc(webmiddleware.Recover()),
		mux.MiddlewareFunc(webmiddleware.FillContext(options.contextFillers...)),
		mux.MiddlewareFunc(webmiddleware.Peer()),
		mux.MiddlewareFunc(webmiddleware.RequestURL()),
		mux.MiddlewareFunc(webmiddleware.RequestID()),
		mux.MiddlewareFunc(webmiddleware.ProxyHeaders(proxyConfiguration)),
		mux.MiddlewareFunc(webmiddleware.Metadata("X-Forwarded-For", "User-Agent")),
		mux.MiddlewareFunc(webmiddleware.MaxBody(1<<24)),  
		mux.MiddlewareFunc(webmiddleware.SecurityHeaders()),
		mux.MiddlewareFunc(webmiddleware.Log(logger, options.logIgnorePaths)),
		mux.MiddlewareFunc(webmiddleware.Cookies(hashKey, blockKey)),
	)
	var redirectConfig webmiddleware.RedirectConfiguration
	if options.redirectToHost != "" {
		if host, portStr, err := net.SplitHostPort(options.redirectToHost); err == nil {
			redirectConfig.HostName = func(string) string { return host }
			port, err := strconv.ParseUint(portStr, 10, 0)
			if err != nil {
				return nil, err
			}
			redirectConfig.Port = func(uint) uint { return uint(port) }
		} else {
			redirectConfig.HostName = func(string) string { return options.redirectToHost }
		}
	}
	if options.redirectToHTTPS != nil {
		redirectConfig.Scheme = func(string) string { return "https" }
		if redirectConfig.Port == nil {
			redirectConfig.Port = func(current uint) uint {
				return uint(options.redirectToHTTPS[int(current)])
			}
		}
	}
	router := root.NewRoute().Subrouter()
	router.Use(
		mux.MiddlewareFunc(webmiddleware.Redirect(redirectConfig)),
	)
	apiRouter := mux.NewRouter()
	apiRouter.NotFoundHandler = http.HandlerFunc(webhandlers.NotFound)
	apiRouter.Use(
		mux.MiddlewareFunc(webmiddleware.CookieAuth("_session")),
		mux.MiddlewareFunc(webmiddleware.CSRF(
			hashKey,
			csrf.CookieName("_csrf"),
			csrf.FieldName("_csrf"),
			csrf.Path("/"),
		)),
		mux.MiddlewareFunc(
			webmiddleware.CORS(webmiddleware.CORSConfig{
				AllowedHeaders: []string{"Authorization", "Content-Type", "X-CSRF-Token"},
				AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete},
				AllowedOrigins: []string{"*"},
				ExposedHeaders: []string{
					"Date",
					"Content-Length",
					"X-Rate-Limit-Limit",
					"X-Rate-Limit-Available",
					"X-Rate-Limit-Reset",
					"X-Rate-Limit-Retry",
					"X-Request-Id",
					"X-Total-Count",
					"X-Warning",
				},
				MaxAge: 600,
			}),
		),
	)
	root.PathPrefix("/api/").Handler(apiRouter)
	server := echo.New()
	server.Logger = &noopLogger{}
	server.HTTPErrorHandler = errorHandler
	server.Use(
		echomiddleware.Gzip(),
	)
	s := &Server{
		root:      root,
		router:    router,
		apiRouter: apiRouter,
		echo:      server,
	}
	var staticPath string
	for _, path := range options.staticSearchPaths {
		if s, err := os.Stat(path); err == nil && s.IsDir() {
			staticPath = path
			break
		}
	}
	if staticPath != "" {
		staticDir := http.Dir(staticPath)
		logger := logger.WithFields(log.Fields("path", staticDir, "mount", options.staticMount))
		s.Static(options.staticMount, staticDir)
		manifest, err := ioutil.ReadFile(filepath.Join(staticPath, "manifest.yaml"))
		if err != nil {
			logger.WithError(err).Warn("Failed to load manifest.yaml")
			return s, nil
		}
		hashedFiles := make(map[string]string)
		err = yaml.Unmarshal(manifest, &hashedFiles)
		if err != nil {
			return nil, errors.New("Corrupted manifest.yaml").WithCause(err)
		}
		for original, hashed := range hashedFiles {
			webui.RegisterHashedFile(original, hashed)
		}
		logger.Debug("Loaded manifest.yaml")
		logger.Debug("Serving static assets")
	} else {
		logger.WithField("search_paths", options.staticSearchPaths).Warn("No static assets found in any search path")
	}
	return s, nil
}
func isZeros(buf []byte) bool {
	for _, b := range buf {
		if b != 0x00 {
			return false
		}
	}
	return true
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.root.ServeHTTP(w, r)
}
func (s *Server) RootRouter() *mux.Router {
	return s.root
}
func (s *Server) Router() *mux.Router {
	return s.router
}
func (s *Server) APIRouter() *mux.Router {
	return s.apiRouter
}
func (s *Server) getRouter(path string) *mux.Router {
	if strings.HasPrefix(path, "/api/") {
		return s.apiRouter
	}
	return s.router
}
var echoVar = regexp.MustCompile(":[^/]+")
func replaceEchoVars(path string) string {
	return echoVar.ReplaceAllStringFunc(path, func(s string) string {
		return fmt.Sprintf("{%s}", strings.TrimPrefix(s, ":"))
	})
}
func (s *Server) Group(prefix string, middleware ...echo.MiddlewareFunc) *echo.Group {
	path := "/" + strings.Trim(prefix, "/")
	pathWithSlash := path + "/"
	router := s.getRouter(pathWithSlash)
	router.PathPrefix(replaceEchoVars(pathWithSlash)).Handler(s.echo)
	if !strings.HasSuffix(path, "/") {
		router.Handle(replaceEchoVars(path), http.RedirectHandler(pathWithSlash, http.StatusPermanentRedirect))
	}
	return s.echo.Group(path, middleware...)
}