Webhook that injects secrets into pods

This commit is contained in:
jillianwilson
2021-10-14 13:06:07 -03:00
parent a5f4a7a0c1
commit a8e6a4a4f1
117 changed files with 30234 additions and 7078 deletions

View File

@@ -1,14 +0,0 @@
# vektor API
`vk` is the vektor component that allows for easy development of API servers in Go.
Features:
- HTTPS by default using LetsEncrypt
- Easy configuration of CORS
- Built in logging
- Authentication plug-in point
- Fast HTTP router built in
Planned:
- Rate limiter

View File

@@ -1,76 +0,0 @@
package vk
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
"github.com/suborbital/vektor/vlog"
)
// ctxKey is a type to represent a key in the Ctx context.
type ctxKey string
// Ctx serves a similar purpose to context.Context, but has some typed fields
type Ctx struct {
Context context.Context
Log *vlog.Logger
Params httprouter.Params
RespHeaders http.Header
requestID string
scope interface{}
}
// NewCtx creates a new Ctx
func NewCtx(log *vlog.Logger, params httprouter.Params, headers http.Header) *Ctx {
ctx := &Ctx{
Context: context.Background(),
Log: log,
Params: params,
RespHeaders: headers,
}
return ctx
}
// Set sets a value on the Ctx's embedded Context (a la key/value store)
func (c *Ctx) Set(key string, val interface{}) {
realKey := ctxKey(key)
c.Context = context.WithValue(c.Context, realKey, val)
}
// Get gets a value from the Ctx's embedded Context (a la key/value store)
func (c *Ctx) Get(key string) interface{} {
realKey := ctxKey(key)
val := c.Context.Value(realKey)
return val
}
// UseScope sets an object to be the scope of the request, including setting the logger's scope
// the scope can be retrieved later with the Scope() method
func (c *Ctx) UseScope(scope interface{}) {
c.Log = c.Log.CreateScoped(scope)
c.scope = scope
}
// Scope retrieves the context's scope
func (c *Ctx) Scope() interface{} {
return c.scope
}
// UseRequestID is a setter for the request ID
func (c *Ctx) UseRequestID(id string) {
c.requestID = id
}
// RequestID returns the request ID of the current request, generating one if none exists.
func (c *Ctx) RequestID() string {
if c.requestID == "" {
c.requestID = uuid.New().String()
}
return c.requestID
}

View File

@@ -1,91 +0,0 @@
package vk
import (
"encoding/json"
"fmt"
"github.com/suborbital/vektor/vlog"
)
// Error is an interface representing a failed request
type Error interface {
Error() string // this ensures all Errors will also conform to the normal error interface
Message() string
Status() int
}
// ErrorResponse is a concrete implementation of Error,
// representing a failed HTTP request
type ErrorResponse struct {
StatusCode int `json:"status"`
MessageText string `json:"message"`
}
// Error returns a full error string
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("%d: %s", e.StatusCode, e.MessageText)
}
// Status returns the error status code
func (e *ErrorResponse) Status() int {
return e.StatusCode
}
// Message returns the error's message
func (e *ErrorResponse) Message() string {
return e.MessageText
}
// Err returns an error with status and message
func Err(status int, message string) Error {
e := &ErrorResponse{
StatusCode: status,
MessageText: message,
}
return e
}
// E is Err for those who like terse code
func E(status int, message string) Error {
return Err(status, message)
}
// Wrap wraps an error in vk.Error
func Wrap(status int, err error) Error {
return Err(status, err.Error())
}
var (
genericErrorResponseBytes = []byte("Internal Server Error")
genericErrorResponseCode = 500
)
// converts _something_ into bytes, best it can:
// if data is Error type, returns (status, {status: status, message: message})
// if other error, returns (500, []byte(err.Error()))
func errorOrOtherToBytes(l *vlog.Logger, err error) (int, []byte, contentType) {
statusCode := genericErrorResponseCode
// first, check if it's vk.Error interface type, and unpack it for further processing
if e, ok := err.(Error); ok {
statusCode = e.Status() // grab this in case anything fails
errResp := Err(e.Status(), e.Message()) // create a concrete instance that can be marshalled
errJSON, marshalErr := json.Marshal(errResp)
if marshalErr != nil {
// any failure results in the generic response body being used
l.ErrorString("failed to marshal vk.Error:", marshalErr.Error(), "original error:", err.Error())
return statusCode, genericErrorResponseBytes, contentTypeTextPlain
}
return statusCode, errJSON, contentTypeJSON
}
l.Warn("redacting potential unsafe error response, original error:", err.Error())
return statusCode, genericErrorResponseBytes, contentTypeTextPlain
}

View File

@@ -1,140 +0,0 @@
package vk
import (
"fmt"
"net/http"
"strings"
)
// RouteGroup represents a group of routes
type RouteGroup struct {
prefix string
routes []routeHandler
middleware []Middleware
afterware []Afterware
}
type routeHandler struct {
Method string
Path string
Handler HandlerFunc
}
// Group creates a group of routes with a common prefix and middlewares
func Group(prefix string) *RouteGroup {
rg := &RouteGroup{
prefix: prefix,
routes: []routeHandler{},
middleware: []Middleware{},
afterware: []Afterware{},
}
return rg
}
// GET is a shortcut for server.Handle(http.MethodGet, path, handler)
func (g *RouteGroup) GET(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodGet, path, handler)
}
// HEAD is a shortcut for server.Handle(http.MethodHead, path, handler)
func (g *RouteGroup) HEAD(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodHead, path, handler)
}
// OPTIONS is a shortcut for server.Handle(http.MethodOptions, path, handler)
func (g *RouteGroup) OPTIONS(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodOptions, path, handler)
}
// POST is a shortcut for server.Handle(http.MethodPost, path, handler)
func (g *RouteGroup) POST(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPost, path, handler)
}
// PUT is a shortcut for server.Handle(http.MethodPut, path, handler)
func (g *RouteGroup) PUT(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPut, path, handler)
}
// PATCH is a shortcut for server.Handle(http.MethodPatch, path, handler)
func (g *RouteGroup) PATCH(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodPatch, path, handler)
}
// DELETE is a shortcut for server.Handle(http.MethodDelete, path, handler)
func (g *RouteGroup) DELETE(path string, handler HandlerFunc) {
g.addRouteHandler(http.MethodDelete, path, handler)
}
// Handle adds a route to be handled
func (g *RouteGroup) Handle(method, path string, handler HandlerFunc) {
g.addRouteHandler(method, path, handler)
}
// AddGroup adds a group of routes to this group as a subgroup.
// the subgroup's prefix is added to all of the routes it contains,
// with the resulting path being "/group.prefix/subgroup.prefix/route/path/here"
func (g *RouteGroup) AddGroup(group *RouteGroup) {
g.routes = append(g.routes, group.routeHandlers()...)
}
// Before adds middleware to the group, which are applied to every handler in the group (called before the handler)
func (g *RouteGroup) Before(middleware ...Middleware) *RouteGroup {
g.middleware = append(g.middleware, middleware...)
return g
}
// After adds afterware to the group, which are applied to every handler in the group (called after the handler)
func (g *RouteGroup) After(afterware ...Afterware) *RouteGroup {
g.afterware = append(g.afterware, afterware...)
return g
}
// routeHandlers computes the "full" path for each handler, and creates
// a HandlerFunc that chains together the group's middlewares
// before calling the inner HandlerFunc. It can be called 'recursively'
// since groups can be added to groups
func (g *RouteGroup) routeHandlers() []routeHandler {
routes := make([]routeHandler, len(g.routes))
for i, r := range g.routes {
fullPath := fmt.Sprintf("%s%s", ensureLeadingSlash(g.prefix), ensureLeadingSlash(r.Path))
augR := routeHandler{
Method: r.Method,
Path: fullPath,
Handler: augmentHandler(r.Handler, g.middleware, g.afterware),
}
routes[i] = augR
}
return routes
}
func (g *RouteGroup) addRouteHandler(method string, path string, handler HandlerFunc) {
rh := routeHandler{
Method: method,
Path: path,
Handler: handler,
}
g.routes = append(g.routes, rh)
}
func (g *RouteGroup) routePrefix() string {
return g.prefix
}
func ensureLeadingSlash(path string) string {
if path == "" {
// handle the "root group" case
return ""
} else if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return path
}

View File

@@ -1,80 +0,0 @@
package vk
import (
"net/http"
)
// Middleware represents a handler that runs on a request before reaching its handler
type Middleware func(*http.Request, *Ctx) error
// Afterware represents a handler that runs on a request after the handler has dealt with the request
type Afterware func(*http.Request, *Ctx)
// ContentTypeMiddleware allows the content-type to be set
func ContentTypeMiddleware(contentType string) Middleware {
return func(r *http.Request, ctx *Ctx) error {
ctx.RespHeaders.Set(contentTypeHeaderKey, contentType)
return nil
}
}
// CORSMiddleware enables CORS with the given domain for a route
// pass "*" to allow all domains, or empty string to allow none
func CORSMiddleware(domain string) Middleware {
return func(r *http.Request, ctx *Ctx) error {
enableCors(ctx, domain)
return nil
}
}
// CORSHandler enables CORS for a route
// pass "*" to allow all domains, or empty string to allow none
func CORSHandler(domain string) HandlerFunc {
return func(r *http.Request, ctx *Ctx) (interface{}, error) {
enableCors(ctx, domain)
return nil, nil
}
}
func enableCors(ctx *Ctx, domain string) {
if domain != "" {
ctx.RespHeaders.Set("Access-Control-Allow-Origin", domain)
ctx.RespHeaders.Set("X-Requested-With", "XMLHttpRequest")
ctx.RespHeaders.Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, cache-control")
}
}
func loggerMiddleware() Middleware {
return func(r *http.Request, ctx *Ctx) error {
ctx.Log.Info(r.Method, r.URL.String())
return nil
}
}
// generate a HandlerFunc that passes the request through a set of Middleware first and Afterware after
func augmentHandler(inner HandlerFunc, middleware []Middleware, afterware []Afterware) HandlerFunc {
return func(r *http.Request, ctx *Ctx) (interface{}, error) {
defer func() {
// run the afterware (which cannot affect the response)
// even if something in the request chain fails
for _, a := range afterware {
a(r, ctx)
}
}()
// run the middleware (which can error to stop progression)
for _, m := range middleware {
if err := m(r, ctx); err != nil {
return nil, err
}
}
resp, err := inner(r, ctx)
return resp, err
}
}

View File

@@ -1,73 +0,0 @@
package vk
import (
"crypto/tls"
"net/http"
"github.com/suborbital/vektor/vlog"
)
// OptionsModifier takes an options struct and returns a modified Options struct
type OptionsModifier func(*Options)
// UseDomain sets the server to use a particular domain for TLS
func UseDomain(domain string) OptionsModifier {
return func(o *Options) {
o.Domain = domain
}
}
// UseTLSConfig sets a TLS config that will be used for HTTPS
// This will take precedence over the Domain option in all cases
func UseTLSConfig(config *tls.Config) OptionsModifier {
return func(o *Options) {
o.TLSConfig = config
}
}
// UseTLSPort sets the HTTPS port to be used:
func UseTLSPort(port int) OptionsModifier {
return func(o *Options) {
o.TLSPort = port
}
}
// UseHTTPPort sets the HTTP port to be used:
// If domain is set, HTTP port will be used for LetsEncrypt challenge server
// If domain is NOT set, this option will put VK in insecure HTTP mode
func UseHTTPPort(port int) OptionsModifier {
return func(o *Options) {
o.HTTPPort = port
}
}
// UseLogger allows a custom logger to be used
func UseLogger(logger *vlog.Logger) OptionsModifier {
return func(o *Options) {
o.Logger = logger
}
}
// UseAppName allows an app name to be set (for vanity only, really....)
func UseAppName(name string) OptionsModifier {
return func(o *Options) {
o.AppName = name
}
}
// UseEnvPrefix uses the provided env prefix (default VK) when looking up other options such as `VK_HTTP_PORT`
func UseEnvPrefix(prefix string) OptionsModifier {
return func(o *Options) {
o.EnvPrefix = prefix
}
}
// UseInspector sets a function that will be allowed to inspect every HTTP request
// before it reaches VK's internal router, but cannot modify said request or affect
// the handling of said request in any way. Use at your own risk, as it may introduce
// performance issues if not used correctly.
func UseInspector(isp func(http.Request)) OptionsModifier {
return func(o *Options) {
o.PreRouterInspector = isp
}
}

View File

@@ -1,95 +0,0 @@
package vk
import (
"context"
"crypto/tls"
"net/http"
"github.com/pkg/errors"
"github.com/sethvargo/go-envconfig"
"github.com/suborbital/vektor/vlog"
)
// Options are the available options for Server
type Options struct {
AppName string `env:"_APP_NAME"`
Domain string `env:"_DOMAIN"`
HTTPPort int `env:"_HTTP_PORT"`
TLSPort int `env:"_TLS_PORT"`
TLSConfig *tls.Config `env:"-"`
EnvPrefix string `env:"-"`
Logger *vlog.Logger `env:"-"`
PreRouterInspector func(http.Request) `env:"-"`
}
func newOptsWithModifiers(mods ...OptionsModifier) *Options {
options := &Options{}
// loop through the provided options and apply the
// modifier function to the options object
for _, mod := range mods {
mod(options)
}
envPrefix := defaultEnvPrefix
if options.EnvPrefix != "" {
envPrefix = options.EnvPrefix
}
options.finalize(envPrefix)
return options
}
// ShouldUseTLS returns true if domain is set and/or TLS is configured
func (o *Options) ShouldUseTLS() bool {
return o.Domain != "" || o.TLSConfig != nil
}
// HTTPPortSet returns true if the HTTP port is set
func (o *Options) HTTPPortSet() bool {
return o.HTTPPort != 0
}
// ShouldUseHTTP returns true if insecure HTTP should be used
func (o *Options) ShouldUseHTTP() bool {
return !o.ShouldUseTLS() && o.HTTPPortSet()
}
// finalize "locks in" the options by overriding any existing options with the version from the environment, and setting the default logger if needed
func (o *Options) finalize(prefix string) {
if o.Logger == nil {
o.Logger = vlog.Default(vlog.EnvPrefix(prefix))
}
// if no inspector was set, create an empty one
if o.PreRouterInspector == nil {
o.PreRouterInspector = func(_ http.Request) {}
}
envOpts := Options{}
if err := envconfig.ProcessWith(context.Background(), &envOpts, envconfig.PrefixLookuper(prefix, envconfig.OsLookuper())); err != nil {
o.Logger.Error(errors.Wrap(err, "[vk] failed to ProcessWith environment config"))
return
}
o.replaceFieldsIfNeeded(&envOpts)
}
func (o *Options) replaceFieldsIfNeeded(replacement *Options) {
if replacement.AppName != "" {
o.AppName = replacement.AppName
}
if replacement.Domain != "" {
o.Domain = replacement.Domain
}
if replacement.HTTPPort != 0 {
o.HTTPPort = replacement.HTTPPort
}
if replacement.TLSPort != 0 {
o.TLSPort = replacement.TLSPort
}
}

View File

@@ -1,77 +0,0 @@
package vk
import (
"encoding/json"
"net/http"
"github.com/pkg/errors"
"github.com/suborbital/vektor/vlog"
)
// Response represents a non-error HTTP response
type Response struct {
status int
body interface{}
}
// Respond returns a filled-in response
func Respond(status int, body interface{}) Response {
r := Response{
status: status,
body: body,
}
return r
}
// R is `Respond` for those who prefer terse code
func R(status int, body interface{}) Response {
return Respond(status, body)
}
// TODO: add convenience helpers for status codes
const (
contentTypeJSON contentType = "application/json"
contentTypeTextPlain contentType = "text/plain"
contentTypeOctetStream contentType = "application/octet-stream"
)
// converts _something_ into bytes, best it can:
// if data is Response type, returns (status, body processed as below)
// if bytes, return (200, bytes)
// if string, return (200, []byte(string))
// if struct, return (200, json(struct))
// otherwise, return (500, nil)
func responseOrOtherToBytes(l *vlog.Logger, data interface{}) (int, []byte, contentType) {
if data == nil {
return http.StatusNoContent, []byte{}, contentTypeTextPlain
}
statusCode := http.StatusOK
realData := data
// first, check if it's response type, and unpack it for further processing
if r, ok := data.(Response); ok {
statusCode = r.status
realData = r.body
}
// if data is []byte or string, return it as-is
if b, ok := realData.([]byte); ok {
return statusCode, b, contentTypeOctetStream
} else if s, ok := realData.(string); ok {
return statusCode, []byte(s), contentTypeTextPlain
}
// otherwise, assume it's a struct of some kind,
// so JSON marshal it and return it
json, err := json.Marshal(realData)
if err != nil {
l.Error(errors.Wrap(err, "failed to Marshal response struct"))
return genericErrorResponseCode, []byte(genericErrorResponseBytes), contentTypeTextPlain
}
return statusCode, json, contentTypeJSON
}

View File

@@ -1,144 +0,0 @@
package vk
import (
"fmt"
"net/http"
"sync"
"github.com/julienschmidt/httprouter"
"github.com/suborbital/vektor/vlog"
)
const contentTypeHeaderKey = "Content-Type"
// used internally to convey content types
type contentType string
// HandlerFunc is the vk version of http.HandlerFunc
// instead of exposing the ResponseWriter, the function instead returns
// an object and an error, which are handled as described in `With` below
type HandlerFunc func(*http.Request, *Ctx) (interface{}, error)
// Router handles the responses on behalf of the server
type Router struct {
*RouteGroup // the "root" RouteGroup that is mounted at server start
hrouter *httprouter.Router // the internal 'actual' router
finalizeOnce sync.Once // ensure that the root only gets mounted once
log *vlog.Logger
}
type defaultScope struct {
RequestID string `json:"request_id"`
}
// NewRouter creates a new Router
func NewRouter(logger *vlog.Logger) *Router {
// add the logger middleware
middleware := []Middleware{loggerMiddleware()}
r := &Router{
RouteGroup: Group("").Before(middleware...),
hrouter: httprouter.New(),
finalizeOnce: sync.Once{},
log: logger,
}
return r
}
// HandleHTTP handles a classic Go HTTP handlerFunc
func (rt *Router) HandleHTTP(method, path string, handler http.HandlerFunc) {
rt.hrouter.Handle(method, path, func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
handler(w, r)
})
}
// Finalize mounts the root group to prepare the Router to handle requests
func (rt *Router) Finalize() {
rt.finalizeOnce.Do(func() {
rt.mountGroup(rt.RouteGroup)
})
}
//ServeHTTP serves HTTP requests
func (rt *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// check to see if the router has a handler for this path
handler, params, _ := rt.hrouter.Lookup(r.Method, r.URL.Path)
if handler != nil {
handler(w, r, params)
} else {
rt.log.Debug("not handled:", r.Method, r.URL.String())
// let httprouter handle the fallthrough cases
rt.hrouter.ServeHTTP(w, r)
}
}
// mountGroup adds a group of handlers to the httprouter
func (rt *Router) mountGroup(group *RouteGroup) {
for _, r := range group.routeHandlers() {
rt.log.Debug("mounting route", r.Method, r.Path)
rt.hrouter.Handle(r.Method, r.Path, rt.handleWrap(r.Handler))
}
}
// handleWrap returns an httprouter.Handle that uses the `inner` vk.HandleFunc to handle the request
//
// inner returns a body and an error;
// the body can can be:
// - a vk.Response object (status and body are written to w)
// - []byte (written directly to w, status 200)
// - a struct (marshalled to JSON and written to w, status 200)
//
// the error can be:
// - a vk.Error type (status and message are written to w)
// - any other error object (status 500 and error.Error() are written to w)
//
func (rt *Router) handleWrap(inner HandlerFunc) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
var status int
var body []byte
var detectedCType contentType
// create a context handleWrap the configured logger
// (and use the ctx.Log for all remaining logging
// in case a scope was set on it)
ctx := NewCtx(rt.log, params, w.Header())
ctx.UseScope(defaultScope{ctx.RequestID()})
resp, err := inner(r, ctx)
if err != nil {
status, body, detectedCType = errorOrOtherToBytes(ctx.Log, err)
} else {
status, body, detectedCType = responseOrOtherToBytes(ctx.Log, resp)
}
// check if anything in the handler chain set the content type
// header, and only use the auto-detected value if it wasn't
headerCType := w.Header().Get(contentTypeHeaderKey)
shouldSetCType := headerCType == ""
ctx.Log.Debug("post-handler contenttype:", string(headerCType))
// if no contentType was set in the middleware chain,
// then set it here based on the type detected
if shouldSetCType {
ctx.Log.Debug("setting auto-detected contenttype:", string(detectedCType))
w.Header().Set(contentTypeHeaderKey, string(detectedCType))
}
w.WriteHeader(status)
w.Write(body)
ctx.Log.Info(r.Method, r.URL.String(), fmt.Sprintf("completed (%d: %s)", status, http.StatusText(status)))
}
}
// canHandle returns true if there's a registered handler that can
// handle the method and path provided or not
func (rt *Router) canHandle(method, path string) bool {
handler, _, _ := rt.hrouter.Lookup(method, path)
return handler != nil
}

View File

@@ -1,289 +0,0 @@
package vk
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"sync"
"sync/atomic"
"golang.org/x/crypto/acme/autocert"
)
const defaultEnvPrefix = "VK"
// Server represents a vektor API server
type Server struct {
router *Router
lock sync.RWMutex
started atomic.Value
server *http.Server
options *Options
}
// New creates a new vektor API server
func New(opts ...OptionsModifier) *Server {
options := newOptsWithModifiers(opts...)
router := NewRouter(options.Logger)
s := &Server{
router: router,
lock: sync.RWMutex{},
started: atomic.Value{},
options: options,
}
s.started.Store(false)
// yes this creates a circular reference,
// but the VK server and HTTP server are
// extremely tightly wound together so
// we have to make this compromise
s.server = createGoServer(options, s)
return s
}
// Start starts the server listening
func (s *Server) Start() error {
if s.started.Load().(bool) {
err := errors.New("server already started")
s.options.Logger.Error(err)
return err
}
// lock the router modifiers (GET, POST etc.)
s.started.Store(true)
// mount the root set of routes before starting
s.router.Finalize()
if s.options.AppName != "" {
s.options.Logger.Info("starting", s.options.AppName, "...")
}
s.options.Logger.Info("serving on", s.server.Addr)
if !s.options.HTTPPortSet() && !s.options.ShouldUseTLS() {
s.options.Logger.ErrorString("domain and HTTP port options are both unset, server will start up but fail to acquire a certificate. reconfigure and restart")
} else if s.options.ShouldUseHTTP() {
return s.server.ListenAndServe()
}
return s.server.ListenAndServeTLS("", "")
}
// TestStart "starts" the server for automated testing with vtest
func (s *Server) TestStart() error {
if s.started.Load().(bool) {
err := errors.New("server already started")
s.options.Logger.Error(err)
return err
}
// lock the router modifiers (GET, POST etc.)
s.started.Store(true)
// mount the root set of routes before starting
s.router.Finalize()
if s.options.AppName != "" {
s.options.Logger.Info("starting", s.options.AppName, "in Test Mode...")
}
return nil
}
// ServeHTTP serves HTTP requests using the internal router while allowing
// said router to be swapped out underneath at any time in a thread-safe way
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// run the inspector with a dereferenced pointer
// so that it can view but not change said request
//
// we intentionally run this before the lock as it's
// possible the inspector may trigger a router-swap
// and that would cause a nasty deadlock
s.options.PreRouterInspector(*r)
// now lock to ensure the router isn't being swapped
// out from underneath us while we're serving this req
s.lock.RLock()
defer s.lock.RUnlock()
s.router.ServeHTTP(w, r)
}
// SwapRouter allows swapping VK's router out in realtime while
// continuing to serve requests in the background
func (s *Server) SwapRouter(router *Router) {
router.Finalize()
// lock after Finalizing the router so
// the lock is released as quickly as possible
s.lock.Lock()
defer s.lock.Unlock()
s.router = router
}
// CanHandle returns true if the server can handle a given method and path
func (s *Server) CanHandle(method, path string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
return s.router.canHandle(method, path)
}
// GET is a shortcut for router.Handle(http.MethodGet, path, handle)
func (s *Server) GET(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.GET(path, handler)
}
// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle)
func (s *Server) HEAD(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.HEAD(path, handler)
}
// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle)
func (s *Server) OPTIONS(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.OPTIONS(path, handler)
}
// POST is a shortcut for router.Handle(http.MethodPost, path, handle)
func (s *Server) POST(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.POST(path, handler)
}
// PUT is a shortcut for router.Handle(http.MethodPut, path, handle)
func (s *Server) PUT(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.PUT(path, handler)
}
// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle)
func (s *Server) PATCH(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.PATCH(path, handler)
}
// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle)
func (s *Server) DELETE(path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.DELETE(path, handler)
}
// Handle adds a route to be handled
func (s *Server) Handle(method, path string, handler HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.Handle(method, path, handler)
}
// AddGroup adds a RouteGroup to be handled
func (s *Server) AddGroup(group *RouteGroup) {
if s.started.Load().(bool) {
return
}
s.router.AddGroup(group)
}
// HandleHTTP allows vk to handle a standard http.HandlerFunc
func (s *Server) HandleHTTP(method, path string, handler http.HandlerFunc) {
if s.started.Load().(bool) {
return
}
s.router.HandleHTTP(method, path, handler)
}
func createGoServer(options *Options, handler http.Handler) *http.Server {
if useHTTP := options.ShouldUseHTTP(); useHTTP {
return goHTTPServerWithPort(options, handler)
}
return goTLSServerWithDomain(options, handler)
}
func goTLSServerWithDomain(options *Options, handler http.Handler) *http.Server {
if options.TLSConfig != nil {
options.Logger.Info("configured for HTTPS with custom configuration")
} else if options.Domain != "" {
options.Logger.Info("configured for HTTPS using domain", options.Domain)
}
tlsConfig := options.TLSConfig
if tlsConfig == nil {
m := &autocert.Manager{
Cache: autocert.DirCache("~/.autocert"),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(options.Domain),
}
addr := fmt.Sprintf(":%d", options.HTTPPort)
if options.HTTPPort == 0 {
addr = ":8080"
}
options.Logger.Info("serving TLS challenges on", addr)
go http.ListenAndServe(addr, m.HTTPHandler(nil))
tlsConfig = &tls.Config{GetCertificate: m.GetCertificate}
}
addr := fmt.Sprintf(":%d", options.TLSPort)
if options.TLSPort == 0 {
addr = ":443"
}
s := &http.Server{
Addr: addr,
TLSConfig: tlsConfig,
Handler: handler,
}
return s
}
func goHTTPServerWithPort(options *Options, handler http.Handler) *http.Server {
options.Logger.Warn("configured to use HTTP with no TLS")
s := &http.Server{
Addr: fmt.Sprintf(":%d", options.HTTPPort),
Handler: handler,
}
return s
}