router

package module
v0.0.0-...-8468a59 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 11, 2019 License: MIT Imports: 14 Imported by: 10

README

Build Status GoDoc

nanobox-router

Simple client for creating and updating custom defined http[s] and ws[s] proxies/ssl-termination without restarting a server application.

Status

Stable/Complete

Quickstart

Implement a server in go (run it)

// main.go
package main

import (
  "fmt"
  "time"
  "os"

  "github.com/nanobox-io/nanobox-router"
)

func main() {
  // start http router (use a different port if you wish, just be sure you have permission)
  err := router.StartHTTP("0.0.0.0:8888")
  if err != nil {
    fmt.Printf("Failed to start - %v\n", err)
    os.Exit(1)
  }
  // configure a route
  routes := []router.Route{router.Route{Domain: "test.com", Page: "World, Hello!\n"}}
  // update the routes
  router.UpdateRoutes(routes)

  // do nothing for a minute (give yourself some time to check out your router)
  time.Sleep(time.Minute)
}

Test it

# make sure your ip/port matches that of your server
$ curl -H 'Host: test.com' 127.0.0.1:8888
World, Hello!

Congratulations proxymaster! Be sure to check out the godocs to enhance your app's functionality.

Data types:

Route:

Fields:

  • SubDomain: Subdomain to match on
  • Domain: Domain to match on
  • Path: Path to match on
  • Targets: URIs of servers
  • FwdPath: Path to forward to targets (combined with target path)
  • Page: Page to serve instead of routing to targets

go:

Route{
  SubDomain: "admin",
  Domain: "test.com",
  Path: "/admin*",
  Targets: []string{"http://127.0.0.1:8080/app1","http://127.0.0.2"},
  FwdPath: "/",
  Page: "",
}

json (for end-user implementations):

{
  "subdomain": "admin",
  "domain": "test.com",
  "path": "/admin*",
  "targets": ["http://127.0.0.1:8080/app1","http://127.0.0.2"],
  "fwdpath": "/",
  "page": ""
}
KeyPair:

Fields:

  • Key: Pem style key
  • Cert: Pem style cert

go:

KeyPair{
  Key: "-----BEGIN PRIVATE KEY-----\nMII.../J8\n-----END PRIVATE KEY-----",
  Cert: "-----BEGIN CERTIFICATE-----\nMII...aI=\n-----END CERTIFICATE-----",
}

Contributing

Contributions to the nanobox-router project are welcome and encouraged. Contributions should follow the Nanobox Contribution Process & Guidelines.

Todo

  • Add configurable ErrorLog like net/http/httputils ReverseProxy

Licence

Mozilla Public License Version 2.0

open source

Documentation

Overview

Package router is a client for creating/maintaining http(s) proxies.

Certificates

Certificates are stored as a KeyPair which contains the key and certificate. A tls.Certificate is created and stored separately and used for serving https clients. If a certificate is added, a tls listener will be created, and the X-Forwarded-Proto header will be set to "https" before the request gets proxied to the target. The certificate's Common Name must be set in order for the router to serve it to a matching incoming request.

Start secure routing as follows:

StartTLS("0.0.0.0:443")

Set certificates as follows:

UpdateCerts([]KeyPair{KeyPair{Key: "abcd123", Cert: "1234abc"}})

Get certificates (key/cert pairs) as follows:

keys := Keys()

Routes

Routes have 2 implicit parts: matching criteria and action definitions. The matching portion includes subdomain, domain, and path. The matching algorighm is recursive, so a request to "admin.test.com" would still match the registered route `Route{Domain:"test.com"}`. Precedence is given to routes that match the request's subdomain (if any). Routes with the longest matching path are also prioritized (e.g. a request to "test.com/admin" would match "/admin" in `[]Route{Route{Path:"/"},Route{Path:"/admin"}}` because "/admin" is longer than "/").

The action portion includes targets, fwdpath, and page. If page is specified, it gets served to the client when the request matches that route. Targets is a list of backend servers to proxy to. A target can include a path which, by default, will be prepended to the request's path prior to proxying. If fwdpath is set, it will be appended to any target path and used as the path forwarded to the target.

Start routing as follows:

StartHTTP("0.0.0.0:80")

Set routes as follows:

UpdateRoutes([]Route{Route{Domain: "test.com", Page: "Hello World!\n"}})

Get registered routes as follows:

routes := Routes()

Matching Scenarios

Requests will always match the route with the longest path defined.

ROUTES
  SUB   DOMAIN    PATH   PAGE
  ""    test.com  /      "test"
  ""    test.com  /admin "admin"

CURL
  REQUEST            RESPONSE
  test.com/admin     "admin"
  test.com/admin/me  "admin"
  admin.test.com     "test"
  test.com/admins    "test"

A path can include a "*" at the end to match similar requests.

ROUTES
  SUB   DOMAIN    PATH   PAGE
  ""    test.com  /      "test"
  ""    test.com  /a*    "a is for apple"
  ""    test.com  /b/    "b things"

CURL
  REQUEST            RESPONSE
  test.com/a         "a is for apple"
  test.com/ant       "a is for apple"
  test.com/ant/man   "a is for apple"
  test.com/b         "test"
  test.com/b/bear    "b things"

A subdomain match takes precedence over a domain/path match.

ROUTES
  SUB   DOMAIN    PATH   PAGE
  admin test.com  /      "admin"
  ""    test.com  /bill  "Buffalo Bill"

CURL
  REQUEST              RESPONSE
  admin.test.com/bill  "admin"
  users.test.com/bill  "Buffalo Bill"

If a Route's matcher has a subdomain only, then all requests with that particular subdomain will have the Route's defined action applied.

ROUTES
  SUB   DOMAIN    PATH  PAGE
  admin ""        /     "admin"
  ""    test1.com /     "test1"

CURL
  REQUEST            RESPONSE
  admin.test1.com    "admin"
  admin.test2.com    "admin"

Logging

In order to view logs embedded within nanobox-router, you must:

import "github.com/jcelliott/lumber"

and set the level of logging desired (see lumber docs for more info)

lumber.Level(lumber.LvlInt("INFO"))

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoHealthy []byte

ErrNoHealthy is for setting a custom error message/html page if no servers are healthy

View Source
var ErrNoRoutes []byte

ErrNoRoutes is for setting a custom error message/html page if no routes are configured

View Source
var ErrorHandler http.Handler

allows defining an error and how its handled

View Source
var IgnoreUpstreamCerts bool

ignore checking upstream cert (likely will be more granular later)

Functions

func NewReverseProxy deprecated

func NewReverseProxy(target *url.URL, fwdPath string) *httputil.ReverseProxy

Deprecated: Use NewSingleHostReverseProxy instead

NewReverseProxy is a customized copy of httputil.NewSingleHostReverseProxy that allows optional nginx 'sub_filter'-like behavior (customize "path" forwarded to target)

func NewSingleHostReverseProxy

func NewSingleHostReverseProxy(target *url.URL, fwdPath string, ignore bool, prefixPath string) *httputil.ReverseProxy

NewSingleHostReverseProxy is a customized copy of httputil.NewSingleHostReverseProxy that allows optional nginx 'sub_filter'-like behavior (customize "path" forwarded to target) as well as optionally ignoring upstream cert checking

func ServeWS

func ServeWS(rw http.ResponseWriter, req *http.Request, p *httputil.ReverseProxy)

ServeWS is a combination of ReverseProxy.ServeHTTP from `net/http/httputil` and piping logic from `nanopack/redundis`. It doesn't exactly match rfc spec for html proxying, but since it's more an http connection turned bare tcp, I don't feel bad.

func SetDefaultCert

func SetDefaultCert(cert, key string) error

SetDefaultCert sets the default cert.

func Start

func Start(httpAddress, tlsAddress string) error

Start starts both http and tls servers

func StartHTTP

func StartHTTP(address string) error

Start the Http Listener. Intentionally handles http requests the same way as tls.

func StartHealth

func StartHealth(pulse int)

StartHealth starts the health checking for all registered routes. Only routes with 'Endpoint' defined will get checked. 'pulse' is the delay (in seconds) between health checks (default 60?)

func StartTLS

func StartTLS(addr string) error

Start listening for secure connection. The web server is split out from the much simpler form of

http.ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler)

because we needed to handle multiple certs all at the same time and we needed to be able to change the set of certs without restarting the server this can be done by establishing a tls listener seperate form the http Server.

func UpdateCerts

func UpdateCerts(newKeys []KeyPair) error

UpdateCerts replaces registered certificates with a new set and restart the secure web server

func UpdateRoutes

func UpdateRoutes(newRoutes []Route) error

UpdateRoutes replaces registered routes with a new set and initializes their proxies, if needed

Types

type KeyPair

type KeyPair struct {
	Cert string `json:"cert"`
	Key  string `json:"key"`
}

A KeyPair contains a key and certificate used to create a tls.Certificate

func Keys

func Keys() []KeyPair

Keys returns registered keys

Example
// self-signed key/cert
key := "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDDboW1FcXq8rJX\nDwGZ2+solI9YR73/uqG0tp2WzPIMUSQY1FbvD9GO8wSToWdnDHW9M15eiLrk1TAn\nuo99phAovlw5RAsv5vopCf13MKVuWXaSwp6bB52qqLnr5SI2wtJBe5/+LzqUNq5/\nnfsUH0dEBc6hOOUeQPVcd8zAQJblKzg5O90wplqy5Iki4xfGrcF2paB8D4I91X7e\n+JRRVZA79zSzZ4x/opV/fsyL5tfRxoCNn9wnDH2KPR2k/e+A4Tw1fo6TisH4scSp\nMRLjf4Xg7+M72E7SDQ3/5+9d5egynzjT2LjHty8Le5J4fV42jtCQrB/PGys1B8Cx\npNtjo1gvAgMBAAECggEAXFZ7HF1mPyVeuB2h/wVWrbzLocV78zlGMDFcciTxdHpe\nGNEzJg8OT4FpNyu6xIixlKyRuQ7XZ0mHUC4ooBB3cBjJUFFjC8YRipRqywcUEvh4\nOs1zzQIjL8A64EdKDB+u4ju8E4hTIDZZ6nhFanOA45Xu1GQidVHx3DfKaUfbQ/l9\nX+AesqN+fpQBsxfKvYPtaKH8OMjcpLmlSns96r7IY5GQQv1Egy4M1W+Urljgcqim\nFblFOOIFD65nTLsGz6VhENc7gF/ueIv2hrlMYvSQQIM9IdrzGfCYLWzDhzY1x9r3\nvh9Erqn0rub0Rap5Wi7gdM8KIqJEjzp0mYvv2j9hmQKBgQDgLFkIE5j2AQn4S4+n\nFP9GHwgzrFuYOe9FAuoeIeVwcb6eNU6B2ptL3PJ/Pbd1dHcmef9pXUa2cpMo682D\ndQOc1h4kl9mNIvxVIj9Vu6fW0PrOBavGyJLsas0iKxiwzzF9bMt9aqcDphu/hfbB\nnXk70eRG9rUdn6EmvkbtEzSBbQKBgQDfLY4DMq2hhpHeRdLsxMYT3OPyeOcV9boD\nB3bVkxy61XTzFTaVyh6gWx9gxpY9mmv5yH96e93rQaqs5ScIuXrBTvSBTOyRTTw1\nzoZeiH0jN/nMV4x7sdhcrXo7hu7OjqcWGFzMiAYH44E277mrx56dvAgigrIJgPBY\njjX6w2waiwKBgDEwCekHw8xWtgVRLxgON2T/ciFEdGSWcbXGyfAKp/lgO98i+zLq\n8KBYvqzEsfiHsY0zv6My4E0wHrIf61wo1L4ZDUwiNY4OWyei+BqrrkwoVp/WBrb7\nU6GkXZZdtnE1RTqsIIpIWJUoYXZIwrgBAZTqnRglEeCKIiYKIi3qxN6RAoGBAKxX\nsG/1xbGTirdbsjtW5SNXk8ud483IeUF3lSPuu+PnjK1et01KzQXF+GAyWrjts+4r\nD45VcxUGG7fyKYeKPCplP1lOPu0h+JoQhyEfQ4tb4ZIUFY870joXWOn5FBb8gDkG\nzTrA2+9hl1oGG5p0x59FIf8McFH4eSHZiAPCv4trAoGBANw+K8+qmVCLOIpGGYqd\nRl2c2V35Qf17bXlLhv+fEliCI6ixp1fLfglE0IXcGtnSnnUH2cWpC5dlythEfyPH\nAfnZHDvuJ6K0uDgDq90EmwKyHQxihUF57D6oR6FZ3MPqmj41umeQyxC/HGtJm6po\na1Zn/gvZVeitHeVAeDJfJ/J8\n-----END PRIVATE KEY-----"
cert := "-----BEGIN CERTIFICATE-----\nMIIDbTCCAlWgAwIBAgIJAM/PXFTYkPDoMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJJRDETMBEGA1UECgwKbmFub2JveC5pbzEcMBoGA1UE\nAwwTbmFub2JveC1yb3V0ZXIudGVzdDAeFw0xNjAzMjIxODQyMTJaFw0xNzAzMjIx\nODQyMTJaME0xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJJRDETMBEGA1UECgwKbmFu\nb2JveC5pbzEcMBoGA1UEAwwTbmFub2JveC1yb3V0ZXIudGVzdDCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAMNuhbUVxeryslcPAZnb6yiUj1hHvf+6obS2\nnZbM8gxRJBjUVu8P0Y7zBJOhZ2cMdb0zXl6IuuTVMCe6j32mECi+XDlECy/m+ikJ\n/XcwpW5ZdpLCnpsHnaqouevlIjbC0kF7n/4vOpQ2rn+d+xQfR0QFzqE45R5A9Vx3\nzMBAluUrODk73TCmWrLkiSLjF8atwXaloHwPgj3Vft74lFFVkDv3NLNnjH+ilX9+\nzIvm19HGgI2f3CcMfYo9HaT974DhPDV+jpOKwfixxKkxEuN/heDv4zvYTtINDf/n\n713l6DKfONPYuMe3Lwt7knh9XjaO0JCsH88bKzUHwLGk22OjWC8CAwEAAaNQME4w\nHQYDVR0OBBYEFMRZye+7JAUv7l/44AVnocivjzJ7MB8GA1UdIwQYMBaAFMRZye+7\nJAUv7l/44AVnocivjzJ7MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB\nAH2ygiWZs8pRYWQy6PKj3arci4diFkBiISGTFoAeE1tYkZVE6fM5acPaOV1z7/Fr\nSKeiRhlC7sfcRURaDPDy0of5V83PazQqs3+SNV4KR+O2PNZk6DalKmtwOlNHRKkJ\n5s79rWgqY1wEt4s5atIwVEgdg7WRz41V7WK5Q9IMkFqYVn8MHVKd0k3nuA9ksfXA\nQPBypyOEJGx7EML6Tena/YerpTmcw2Xt4ssxiZQIn/wP3dyqISGark8BNWK6y7iG\nWkt2VZCvKXhb5Q+s4IlxA58InR1b+8/NauYyL1bUgcc3LBHN5Ty6nMUUeb2WPQ32\n4qod6vx2rJfj718EYjrWdaI=\n-----END CERTIFICATE-----"
// configure a cert
certs := []router.KeyPair{router.KeyPair{Key: key, Cert: cert}}

// update the certs
router.UpdateCerts(certs)

// get certs
savedCerts := router.Keys()
if len(savedCerts) < 1 {
	return
}
fmt.Printf("%v\n", savedCerts[0].Key)
Output:

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDDboW1FcXq8rJX
DwGZ2+solI9YR73/uqG0tp2WzPIMUSQY1FbvD9GO8wSToWdnDHW9M15eiLrk1TAn
uo99phAovlw5RAsv5vopCf13MKVuWXaSwp6bB52qqLnr5SI2wtJBe5/+LzqUNq5/
nfsUH0dEBc6hOOUeQPVcd8zAQJblKzg5O90wplqy5Iki4xfGrcF2paB8D4I91X7e
+JRRVZA79zSzZ4x/opV/fsyL5tfRxoCNn9wnDH2KPR2k/e+A4Tw1fo6TisH4scSp
MRLjf4Xg7+M72E7SDQ3/5+9d5egynzjT2LjHty8Le5J4fV42jtCQrB/PGys1B8Cx
pNtjo1gvAgMBAAECggEAXFZ7HF1mPyVeuB2h/wVWrbzLocV78zlGMDFcciTxdHpe
GNEzJg8OT4FpNyu6xIixlKyRuQ7XZ0mHUC4ooBB3cBjJUFFjC8YRipRqywcUEvh4
Os1zzQIjL8A64EdKDB+u4ju8E4hTIDZZ6nhFanOA45Xu1GQidVHx3DfKaUfbQ/l9
X+AesqN+fpQBsxfKvYPtaKH8OMjcpLmlSns96r7IY5GQQv1Egy4M1W+Urljgcqim
FblFOOIFD65nTLsGz6VhENc7gF/ueIv2hrlMYvSQQIM9IdrzGfCYLWzDhzY1x9r3
vh9Erqn0rub0Rap5Wi7gdM8KIqJEjzp0mYvv2j9hmQKBgQDgLFkIE5j2AQn4S4+n
FP9GHwgzrFuYOe9FAuoeIeVwcb6eNU6B2ptL3PJ/Pbd1dHcmef9pXUa2cpMo682D
dQOc1h4kl9mNIvxVIj9Vu6fW0PrOBavGyJLsas0iKxiwzzF9bMt9aqcDphu/hfbB
nXk70eRG9rUdn6EmvkbtEzSBbQKBgQDfLY4DMq2hhpHeRdLsxMYT3OPyeOcV9boD
B3bVkxy61XTzFTaVyh6gWx9gxpY9mmv5yH96e93rQaqs5ScIuXrBTvSBTOyRTTw1
zoZeiH0jN/nMV4x7sdhcrXo7hu7OjqcWGFzMiAYH44E277mrx56dvAgigrIJgPBY
jjX6w2waiwKBgDEwCekHw8xWtgVRLxgON2T/ciFEdGSWcbXGyfAKp/lgO98i+zLq
8KBYvqzEsfiHsY0zv6My4E0wHrIf61wo1L4ZDUwiNY4OWyei+BqrrkwoVp/WBrb7
U6GkXZZdtnE1RTqsIIpIWJUoYXZIwrgBAZTqnRglEeCKIiYKIi3qxN6RAoGBAKxX
sG/1xbGTirdbsjtW5SNXk8ud483IeUF3lSPuu+PnjK1et01KzQXF+GAyWrjts+4r
D45VcxUGG7fyKYeKPCplP1lOPu0h+JoQhyEfQ4tb4ZIUFY870joXWOn5FBb8gDkG
zTrA2+9hl1oGG5p0x59FIf8McFH4eSHZiAPCv4trAoGBANw+K8+qmVCLOIpGGYqd
Rl2c2V35Qf17bXlLhv+fEliCI6ixp1fLfglE0IXcGtnSnnUH2cWpC5dlythEfyPH
AfnZHDvuJ6K0uDgDq90EmwKyHQxihUF57D6oR6FZ3MPqmj41umeQyxC/HGtJm6po
a1Zn/gvZVeitHeVAeDJfJ/J8
-----END PRIVATE KEY-----

type NoHealthy

type NoHealthy struct {
}

func (NoHealthy) ServeHTTP

func (self NoHealthy) ServeHTTP(rw http.ResponseWriter, req *http.Request)

type NoRoutes

type NoRoutes struct {
}

func (NoRoutes) ServeHTTP

func (self NoRoutes) ServeHTTP(rw http.ResponseWriter, req *http.Request)

type Route

type Route struct {
	// defines match characteristics
	SubDomain string `json:"subdomain"` // subdomain to match on - "admin"
	Domain    string `json:"domain"`    // domain to match on - "myapp.com"
	Path      string `json:"path"`      // route to match on - "/admin"
	// defines actions
	Targets []string `json:"targets"` // ips of servers - ["http://127.0.0.1:8080/app1","http://127.0.0.2"] (optional)
	FwdPath string   `json:"fwdpath"` // path to forward to targets - "/goadmin" incoming req: test.com/admin -> 127.0.0.1/goadmin (optional)
	Page    string   `json:"page"`    // page to serve instead of routing to targets - "<HTML>We are fixing it</HTML>" (optional)

	// defines health check
	Endpoint       string `json:"endpoint"`        // url path to check for health (todo: what to do when fwdpath is set) (non blank enables health checks)
	ExpectedCode   int    `json:"expected_code"`   // expected http response code (default 200)
	ExpectedBody   string `json:"expected_body"`   // expected body
	ExpectedHeader string `json:"expected_header"` // expected http header (field:value)
	Host           string `json:"host"`            // 'host' header to use when performing health check
	Timeout        int    `json:"timeout"`         // milliseconds before connection times out (default 3000 (3s)) (health check)
	Attempts       int    `json:"attempts"`        // number of times to try before marking dead (health check)
	// contains filtered or unexported fields
}

A Route contains the routing rules for a specific match. A match is based on the subdomain, domain, and path. If no path is specified, subdomain/domain matching will be used. A path may include the "*" wildcard character at the end of the path ("/admin*") for a broader match.

"Targets" are not required if a "Page" is defined. A target is a list of servers to proxy a request to. A page gets served to clients upon a successful domain/path match. Pages take precedence over targets.

FwdPath is similar to nginx's "sub_filter" as it allows the user to specify what query path gets forwarded to the client.

func Routes

func Routes() []Route

Routes returns registered routes

Example
// configure a route
routes := []router.Route{router.Route{Domain: "nanobox-router.test", Targets: []string{"http://127.0.0.1:8088"}}}

// update the routes
router.UpdateRoutes(routes)

// get routes
savedRoutes := router.Routes()
if len(savedRoutes) < 1 {
	return
}
fmt.Printf("%v\n", savedRoutes[0].Domain)
Output:

nanobox-router.test

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL