nuts-foundation/nuts-node

View on GitHub
http/echo.go

Summary

Maintainability
A
0 mins
Test Coverage
A
91%
/*
 * Copyright (C) 2021 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package http

import (
    "context"
    "errors"
    "fmt"
    "github.com/labstack/echo/v4"
    "github.com/nuts-foundation/nuts-node/core"
    "github.com/sirupsen/logrus"
    "net/http"
    "strings"
    "sync"
)

// RootPath is the path used for routes that don't map to a configured bind.
const RootPath = "/"

// EchoCreator is a function used to create an Echo server.
type EchoCreator func() (EchoServer, error)

// NewMultiEcho creates a new MultiEcho which uses the given function to create core.EchoServers. If a route is registered
// for an unknown path is bound to the given defaultInterface.
func NewMultiEcho() *MultiEcho {
    instance := &MultiEcho{
        interfaces: map[string]EchoServer{},
        binds:      map[string]string{},
    }

    // Add adds a route to the Echo server.
    instance.echoAdapter.useFn = instance.Use
    instance.echoAdapter.addFn = func(method, path string, handler echo.HandlerFunc, middleware ...echo.MiddlewareFunc) *echo.Route {
        bind := instance.getBindFromPath(path)
        bindAddress := instance.binds[bind]
        var iface EchoServer
        if bindAddress != "" {
            iface = instance.interfaces[bindAddress]
        } else {
            iface = instance.interfaces[instance.binds[RootPath]]
        }
        return iface.Add(method, path, handler, middleware...)
    }
    return instance
}

// MultiEcho allows to bind specific URLs to specific HTTP interfaces
type MultiEcho struct {
    echoAdapter

    interfaces map[string]EchoServer
    binds      map[string]string
}

// Bind binds the given path (first part of the URL) to the given HTTP interface. Calling Bind for the same path twice
// results in an error being returned.
// If address wasn't used for another bind and thus leads to creating a new Echo server, it returns true.
// If an existing Echo server is returned, it returns false.
func (c *MultiEcho) Bind(path string, address string, creatorFn func() (EchoServer, error)) error {
    if len(address) == 0 {
        return errors.New("empty address")
    }
    err := c.validateBindPath(path)
    if err != nil {
        return err
    }
    path = c.getBindFromPath(path)
    if _, pathExists := c.binds[path]; pathExists {
        return fmt.Errorf("http bind already exists: %s", path)
    }
    c.binds[path] = address
    if _, addressBound := c.interfaces[address]; !addressBound {
        server, err := creatorFn()
        if err != nil {
            return err
        }
        c.interfaces[address] = server
    }
    return nil
}

// Start starts all Echo servers.
func (c MultiEcho) Start() error {
    wg := &sync.WaitGroup{}
    wg.Add(len(c.interfaces))
    errChan := make(chan error, len(c.interfaces))
    for addr, curr := range c.interfaces {
        go func(addr string, server EchoServer) {
            if err := server.Start(addr); err != nil {
                errChan <- err
            }
            wg.Done()
        }(addr, curr)
    }
    wg.Wait()
    if len(errChan) > 0 {
        return <-errChan
    }
    return nil
}

// Shutdown stops all Echo servers.
func (c MultiEcho) Shutdown(ctx context.Context) error {
    var result error
    for address, curr := range c.interfaces {
        logrus.Tracef("Stopping interface: %s", address)
        if err := curr.Shutdown(ctx); err != nil {
            logrus.
                WithError(err).
                Errorf("Unable to shutdown interface: %s", address)
            result = errors.New("one or more HTTP interfaces failed to shutdown")
        }
    }
    return result
}

// Use applies the given middleware function to all Echo servers.
func (c MultiEcho) Use(middleware ...echo.MiddlewareFunc) {
    for _, curr := range c.interfaces {
        curr.Use(middleware...)
    }
}
func (c *MultiEcho) getAddressForPath(path string) string {
    return c.binds[c.getBindFromPath(path)]
}

func (c *MultiEcho) validateBindPath(path string) error {
    path = strings.Trim(path, "/")
    if strings.Contains(path, "/") {
        return fmt.Errorf("bind can't contain subpaths: %s", path)
    }
    return nil
}

func (c *MultiEcho) getBindFromPath(path string) string {
    path = strings.Trim(path, "/")
    parts := strings.Split(path, "/")
    if len(parts) == 0 {
        path = RootPath
    } else {
        path = "/" + strings.ToLower(parts[0])
    }
    return path
}

// EchoServer implements both the EchoRouter interface and Start function to aid testing.
type EchoServer interface {
    core.EchoRouter
    Start(address string) error
    Shutdown(ctx context.Context) error
}

var _ core.EchoRouter = (*echoAdapter)(nil)

type echoAdapter struct {
    core.EchoRouter
    EchoServer

    startFn    func(address string) error
    shutdownFn func(ctx context.Context) error
    addFn      func(method, path string, handler echo.HandlerFunc, middleware ...echo.MiddlewareFunc) *echo.Route
    useFn      func(middleware ...echo.MiddlewareFunc)
}

// Start calls the start function.
func (c *echoAdapter) Start(address string) error {
    return c.startFn(address)
}

// Shutdown calls the shutdown function.
func (c *echoAdapter) Shutdown(ctx context.Context) error {
    return c.shutdownFn(ctx)
}

// Use calls the use function.
func (c *echoAdapter) Use(middleware ...echo.MiddlewareFunc) {
    c.useFn(middleware...)
}

// Add calls the add function.
func (c *echoAdapter) Add(method, path string, handler echo.HandlerFunc, middleware ...echo.MiddlewareFunc) *echo.Route {
    return c.addFn(method, path, handler, middleware...)
}

// CONNECT registers a new CONNECT route for the given path with optional middleware.
func (c *echoAdapter) CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodConnect, path, h, m...)
}

// DELETE registers a new DELETE route for the given path with optional middleware.
func (c *echoAdapter) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodDelete, path, h, m...)
}

// GET registers a new GET route for the given path with optional middleware.
func (c *echoAdapter) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodGet, path, h, m...)
}

// HEAD registers a new HEAD route for the given path with optional middleware.
func (c *echoAdapter) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodHead, path, h, m...)
}

// OPTIONS registers a new OPTIONS route for the given path with optional middleware.
func (c *echoAdapter) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodOptions, path, h, m...)
}

// PATCH registers a new PATCH route for the given path with optional middleware.
func (c *echoAdapter) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodPatch, path, h, m...)
}

// POST registers a new POST route for the given path with optional middleware.
func (c *echoAdapter) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodPost, path, h, m...)
}

// PUT registers a new PUT route for the given path with optional middleware.
func (c *echoAdapter) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodPut, path, h, m...)
}

// TRACE registers a new TRACE route for the given path with optional middleware.
func (c *echoAdapter) TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
    return c.Add(http.MethodTrace, path, h, m...)
}