feat: Control API uses automatic cross-pinning mTLS (Closes #119)

. kapow server generates on startup a pair of certificates
that will use to secure communications to its control server.
It will communicate the server and client certificates as well
as the client private key to the init programs it launches,
via environment variables.

. kapow server now understands a new flag --control-reachable-addr
which accepts either a IP address or a DNS name, that can be used
to ensure that the generated server certificate will be appropiate
in case the control server must be accessed from something other
than localhost.

Co-authored-by: Roberto Abdelkader Martínez Pérez <robertomartinezp@gmail.com>
This commit is contained in:
pancho horrillo
2021-03-12 17:11:12 +01:00
parent ab50721f69
commit 1e63f3c104
18 changed files with 325 additions and 103 deletions
+3 -4
View File
@@ -62,10 +62,9 @@ You can find the complete documentation and examples [here](https://kapow.readth
## Security
Please consider the following security caveats **before** using *Kapow!*
- [Issue #119](https://github.com/BBVA/kapow/issues/119)
- [Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns)
Please consider the following
[Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns)
**before** using *Kapow!*
If you are not 100% sure about what you are doing we recommend not using *Kapow!*
+9 -5
View File
@@ -16,16 +16,20 @@ By default it binds to address ``0.0.0.0`` and port ``8080``, but that can be
changed via the ``--bind`` flag.
.. _http-control-interface:
.. _https-control-interface:
HTTP Control Interface
----------------------
HTTPS Control Interface
-----------------------
The `HTTP Control Interface` is used by the command ``kapow route`` to
The `HTTPS Control Interface` is used by the command ``kapow route`` to
administer the list of system routes.
This interface uses mTLS by default (double-pinned autogenerated certs).
By default it binds to address ``127.0.0.1`` and port ``8081``, but that can be
changed via the ``--control-bind`` flag.
changed via the ``--control-bind`` flag. If this is the case, consider
also ``--control-reachable-addr`` which will configure the autogenerated
certificate to match that address.
.. _http-data-interface:
+2 -2
View File
@@ -30,8 +30,8 @@ The spawned entrypoint is run with the following variables added to its
environment:
- :envvar:`KAPOW_HANDLER_ID`: Containing the `HANDLER_ID`
- :envvar:`KAPOW_DATAAPI_URL`: With the URL of the :ref:`http-data-interface`
- :envvar:`KAPOW_CONTROLAPI_URL`: With the URL of the :ref:`http-control-interface`
- :envvar:`KAPOW_DATA_URL`: With the URL of the :ref:`http-data-interface`
- :envvar:`KAPOW_CONTROL_URL`: With the URL of the :ref:`https-control-interface`
3. ``kapow set /response/body banana``
+1 -1
View File
@@ -100,7 +100,7 @@ Or, if you want human-readable output, you can use :program:`jq`:
.. note::
*Kapow!* has a :ref:`http-control-interface`, bound by default to
*Kapow!* has a :ref:`https-control-interface`, bound by default to
``localhost:8081``.
+99
View File
@@ -0,0 +1,99 @@
package certs
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"time"
"github.com/BBVA/kapow/internal/logger"
)
type Cert struct {
X509Cert *x509.Certificate
PrivKey crypto.PrivateKey
SignedCert []byte
}
func (c Cert) SignedCertPEMBytes() []byte {
PEM := new(bytes.Buffer)
err := pem.Encode(PEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: c.SignedCert,
})
if err != nil {
logger.L.Fatal(err)
}
return PEM.Bytes()
}
func (c Cert) PrivateKeyPEMBytes() []byte {
PEM := new(bytes.Buffer)
err := pem.Encode(PEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(c.PrivKey.(*rsa.PrivateKey)),
})
if err != nil {
logger.L.Fatal(err)
}
return PEM.Bytes()
}
func GenCert(name, altName string, isServer bool) Cert {
usage := x509.ExtKeyUsageClientAuth
if isServer {
usage = x509.ExtKeyUsageServerAuth
}
var dnsNames []string
var ipAddresses []net.IP
if altName != "" {
if ipAddr := net.ParseIP(altName); ipAddr != nil {
ipAddresses = []net.IP{ipAddr}
} else {
dnsNames = []string{altName}
}
}
cert := &x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: dnsNames,
IPAddresses: ipAddresses,
Subject: pkix.Name{
CommonName: name,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: false,
BasicConstraintsValid: true,
ExtKeyUsage: []x509.ExtKeyUsage{
usage,
},
}
certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
logger.L.Fatal(err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey)
if err != nil {
logger.L.Fatal(err)
}
return Cert{
X509Cert: cert,
PrivKey: certPrivKey,
SignedCert: certBytes,
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package client
import (
"os"
"testing"
"github.com/BBVA/kapow/internal/http"
)
func TestMain(m *testing.M) {
http.ControlClientGenerator = nil
os.Exit(m.Run())
}
+1 -1
View File
@@ -25,5 +25,5 @@ import (
// GetData will perform the request and write the results on the provided writer
func GetData(host, id, path string, w io.Writer) error {
url := host + "/handlers/" + id + path
return http.Get(url, "", nil, w)
return http.Get(url, nil, w, nil)
}
+1 -1
View File
@@ -36,5 +36,5 @@ func AddRoute(host, path, method, entrypoint, command string, w io.Writer) error
payload["entrypoint"] = entrypoint
}
body, _ := json.Marshal(payload)
return http.Post(url, "application/json", bytes.NewReader(body), w)
return http.Post(url, bytes.NewReader(body), w, http.ControlClientGenerator, http.AsJSON)
}
+1 -1
View File
@@ -25,5 +25,5 @@ import (
// ListRoutes queries the kapow! instance for the routes that are registered
func ListRoutes(host string, w io.Writer) error {
url := host + "/routes"
return http.Get(url, "", nil, w)
return http.Get(url, nil, w, http.ControlClientGenerator)
}
+1 -1
View File
@@ -23,5 +23,5 @@ import (
// RemoveRoute removes a registered route in Kapow! server
func RemoveRoute(host, id string) error {
url := host + "/routes/" + id
return http.Delete(url, "", nil, nil)
return http.Delete(url, nil, nil, http.ControlClientGenerator)
}
+1 -1
View File
@@ -24,5 +24,5 @@ import (
func SetData(host, handlerID, path string, r io.Reader) error {
url := host + "/handlers/" + handlerID + path
return http.Put(url, "", r, nil)
return http.Put(url, r, nil, nil)
}
+3 -3
View File
@@ -43,7 +43,7 @@ func init() {
}
},
}
routeListCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL")
routeListCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL")
// TODO: Manage args for url_pattern and command_file (2 exact args)
var routeAddCmd = &cobra.Command{
@@ -78,7 +78,7 @@ func init() {
},
}
// TODO: Add default values for flags and remove path flag
routeAddCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL")
routeAddCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL")
routeAddCmd.Flags().StringP("method", "X", "GET", "HTTP method to accept")
routeAddCmd.Flags().StringP("entrypoint", "e", "", "Command to execute")
routeAddCmd.Flags().StringP("command", "c", "", "Command to pass to the shell")
@@ -95,7 +95,7 @@ func init() {
}
},
}
routeRemoveCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "http://localhost:8081"), "Kapow! control interface URL")
routeRemoveCmd.Flags().String("control-url", getEnv("KAPOW_CONTROL_URL", "https://localhost:8081"), "Kapow! control interface URL")
RouteCmd.AddCommand(routeListCmd)
RouteCmd.AddCommand(routeAddCmd)
+60 -3
View File
@@ -19,16 +19,40 @@ package cmd
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
"github.com/BBVA/kapow/internal/certs"
"github.com/BBVA/kapow/internal/logger"
"github.com/BBVA/kapow/internal/server"
)
func banner() {
fmt.Fprintln(os.Stderr, `
%% %%%%
%%% %%%
%% %%% %%%
%%%%%%% %%% %%% %%% %%%
*%% %%%%%%%%%%%%%%% %%%% %%% %%
%% %%%%%%%%%. %%% %%%% %%% %%%%%%%%
%%%% %%% %%% %%% %%% %%%%%% %%%%
%%% %%% %%%%%% %%% %%%% %%% %%%% %%%% %%% %%%
%%% %%% %% %%% %%%%% %%%%% %%%% %%%
%%% %%% %% %%%%%%%%% %%%%%%%%%%
%%%%%% %%% %%%%%% %%%
%%% %%%%% %% %%%%%%
%%% %%%%%%%
%%%%
% If you can script it, you can HTTP it.
`)
}
// ServerCmd is the command line interface for kapow server
var ServerCmd = &cobra.Command{
Use: "server [optional flags] [optional init program(s)]",
@@ -42,6 +66,8 @@ var ServerCmd = &cobra.Command{
sConf.ControlBindAddr, _ = cmd.Flags().GetString("control-bind")
sConf.DataBindAddr, _ = cmd.Flags().GetString("data-bind")
controlReachableAddr, _ := cmd.Flags().GetString("control-reachable-addr")
sConf.CertFile, _ = cmd.Flags().GetString("certfile")
sConf.KeyFile, _ = cmd.Flags().GetString("keyfile")
@@ -49,29 +75,51 @@ var ServerCmd = &cobra.Command{
sConf.ClientCaFile, _ = cmd.Flags().GetString("clientcafile")
sConf.Debug, _ = cmd.Flags().GetBool("debug")
sConf.ControlServerCert = certs.GenCert("control_server", extractHost(controlReachableAddr), true)
sConf.ControlClientCert = certs.GenCert("control_client", "", false)
// Set environment variables KAPOW_DATA_URL and KAPOW_CONTROL_URL only if they aren't set so we don't overwrite user's preferences
if _, exist := os.LookupEnv("KAPOW_DATA_URL"); !exist {
os.Setenv("KAPOW_DATA_URL", "http://"+sConf.DataBindAddr)
}
if _, exist := os.LookupEnv("KAPOW_CONTROL_URL"); !exist {
os.Setenv("KAPOW_CONTROL_URL", "http://"+sConf.ControlBindAddr)
os.Setenv("KAPOW_CONTROL_URL", "https://"+controlReachableAddr)
}
banner()
server.StartServer(sConf)
for _, path := range args {
go Run(path, sConf.Debug)
go Run(
path,
sConf.Debug,
sConf.ControlServerCert.SignedCertPEMBytes(),
sConf.ControlClientCert.SignedCertPEMBytes(),
sConf.ControlClientCert.PrivateKeyPEMBytes(),
)
}
select {}
},
}
func extractHost(s string) string {
i := strings.LastIndex(s, ":")
s = s[:i]
l := len(s) - 1
if s[0] == '[' && s[l] == ']' {
s = s[1:l]
}
return s
}
func init() {
ServerCmd.Flags().String("bind", "0.0.0.0:8080", "IP address and port to bind the user interface to")
ServerCmd.Flags().String("control-bind", "localhost:8081", "IP address and port to bind the control interface to")
ServerCmd.Flags().String("data-bind", "localhost:8082", "IP address and port to bind the data interface to")
ServerCmd.Flags().String("control-reachable-addr", "localhost:8081", "address (incl. port) through which the control interface can be reached (from the client's point of view)")
ServerCmd.Flags().String("certfile", "", "Cert file to serve thru https")
ServerCmd.Flags().String("keyfile", "", "Key file to serve thru https")
@@ -100,10 +148,19 @@ func validateServerCommandArguments(cmd *cobra.Command, args []string) error {
return nil
}
func Run(path string, debug bool) {
func Run(
path string,
debug bool,
controlServerCertPEM,
controlClientCertPEM,
controlClientCertPrivKeyPEM []byte,
) {
logger.L.Printf("Running init program %+q", path)
cmd := BuildCmd(path)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_SERVER_CERT=%s", controlServerCertPEM))
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_CERT=%s", controlClientCertPEM))
cmd.Env = append(cmd.Env, fmt.Sprintf("KAPOW_CONTROL_CLIENT_KEY=%s", controlClientCertPrivKeyPEM))
var wg sync.WaitGroup
if debug {
+70 -12
View File
@@ -17,30 +17,41 @@
package http
import (
"crypto/tls"
"crypto/x509"
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"github.com/BBVA/kapow/internal/logger"
)
var ControlClientGenerator = GenControlHTTPSClient
func AsJSON(req *http.Request) {
req.Header.Add("Content-Type", "application/json")
}
// Get perform a request using Request with the GET method
func Get(url string, contentType string, r io.Reader, w io.Writer) error {
return Request("GET", url, contentType, r, w)
func Get(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
return Request("GET", url, r, w, clientGenerator, reqTuner...)
}
// Post perform a request using Request with the POST method
func Post(url string, contentType string, r io.Reader, w io.Writer) error {
return Request("POST", url, contentType, r, w)
func Post(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
return Request("POST", url, r, w, clientGenerator, reqTuner...)
}
// Put perform a request using Request with the PUT method
func Put(url string, contentType string, r io.Reader, w io.Writer) error {
return Request("PUT", url, contentType, r, w)
func Put(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
return Request("PUT", url, r, w, clientGenerator, reqTuner...)
}
// Delete perform a request using Request with the DELETE method
func Delete(url string, contentType string, r io.Reader, w io.Writer) error {
return Request("DELETE", url, contentType, r, w)
func Delete(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
return Request("DELETE", url, r, w, clientGenerator, reqTuner...)
}
var devnull = ioutil.Discard
@@ -49,17 +60,24 @@ var devnull = ioutil.Discard
// content of the given reader as the body and writing all the contents
// of the response to the given writer. The reader and writer are
// optional.
func Request(method string, url string, contentType string, r io.Reader, w io.Writer) error {
func Request(method string, url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuners ...func(*http.Request)) error {
req, err := http.NewRequest(method, url, r)
if err != nil {
return err
}
if contentType != "" {
req.Header.Add("Content-Type", contentType)
for _, reqTuner := range reqTuners {
reqTuner(req)
}
res, err := new(http.Client).Do(req)
var client *http.Client
if clientGenerator == nil {
client = new(http.Client)
} else {
client = clientGenerator()
}
res, err := client.Do(req)
if err != nil {
return err
}
@@ -81,3 +99,43 @@ func Request(method string, url string, contentType string, r io.Reader, w io.Wr
return err
}
func GenControlHTTPSClient() *http.Client {
serverCert, exists := os.LookupEnv("KAPOW_CONTROL_SERVER_CERT")
if !exists {
logger.L.Fatal("KAPOW_CONTROL_SERVER_CERT not in the environment")
}
clientCert, exists := os.LookupEnv("KAPOW_CONTROL_CLIENT_CERT")
if !exists {
logger.L.Fatal("KAPOW_CONTROL_CLIENT_CERT not in the environment")
}
clientKey, exists := os.LookupEnv("KAPOW_CONTROL_CLIENT_KEY")
if !exists {
logger.L.Fatal("KAPOW_CONTROL_CLIENT_KEY not in the environment")
}
// Load client cert
clientTLSCert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
if err != nil {
logger.L.Fatal(err)
}
// Load Server cert
serverCertPool := x509.NewCertPool()
serverCertPool.AppendCertsFromPEM([]byte(serverCert))
// Setup HTTPS client
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientTLSCert},
RootCAs: serverCertPool,
}
tlsConfig.BuildNameToCertificate()
transport := &http.Transport{TLSClientConfig: tlsConfig}
client := &http.Client{Transport: transport}
// The client is always right!
return client
}
+13 -14
View File
@@ -29,7 +29,7 @@ func TestReturnErrorOnInvalidURL(t *testing.T) {
defer gock.Off()
gock.New("").Reply(200)
err := Request("GET", "://", "", nil, nil)
err := Request("GET", "://", nil, nil, nil)
if err == nil {
t.Errorf("Expected error not returned")
}
@@ -45,7 +45,7 @@ func TestRequestGivenMethod(t *testing.T) {
mock.Method = "FOO"
mock.Reply(200)
err := Request("FOO", "http://localhost", "", nil, nil)
err := Request("FOO", "http://localhost", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error on request")
}
@@ -60,7 +60,7 @@ func TestReturnHTTPErrorAsIs(t *testing.T) {
customError := errors.New("FOO")
gock.New("http://localhost").ReplyError(customError)
err := Request("GET", "http://localhost", "", nil, nil)
err := Request("GET", "http://localhost", nil, nil, nil)
if errors.Unwrap(err) != customError {
t.Errorf("Returned error is not the expected error: '%v'", err)
}
@@ -76,7 +76,7 @@ func TestReturnHTTPReasonAsErrorWhenUnsuccessful(t *testing.T) {
Reply(http.StatusTeapot).
BodyString(`{"reason": "I'm a teapot"}`)
err := Request("GET", "http://localhost", "", nil, nil)
err := Request("GET", "http://localhost", nil, nil, nil)
if err == nil || err.Error() != http.StatusText(http.StatusTeapot) {
t.Errorf("Reason should be returned as an error")
}
@@ -93,7 +93,7 @@ func TestCopyResponseBodyToWriter(t *testing.T) {
rw := new(bytes.Buffer)
err := Request("GET", "http://localhost", "", nil, rw)
err := Request("GET", "http://localhost", nil, rw, nil)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
@@ -119,7 +119,7 @@ func TestWriteToDevNullWhenNoWriter(t *testing.T) {
defer func() { devnull = original }()
err := Request("GET", "http://localhost", "", nil, nil)
err := Request("GET", "http://localhost", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
@@ -135,14 +135,13 @@ func TestWriteToDevNullWhenNoWriter(t *testing.T) {
}
}
func TestSendContentType(t *testing.T) {
func TestSendContentTypeJSON(t *testing.T) {
defer gock.Off()
gock.New("http://localhost").
MatchHeader("Content-Type", "foo/bar").
HeaderPresent("Content-Type").
MatchHeader("Content-Type", "application/json").
Reply(http.StatusOK)
err := Request("GET", "http://localhost", "foo/bar", nil, nil)
err := Request("GET", "http://localhost", nil, nil, nil, AsJSON)
if err != nil {
t.Errorf("Unexpected error '%v'", err.Error())
}
@@ -158,7 +157,7 @@ func TestGetRequestsWithMethodGet(t *testing.T) {
Get("/").
Reply(http.StatusOK)
err := Get("http://localhost/", "", nil, nil)
err := Get("http://localhost/", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error %q", err)
@@ -175,7 +174,7 @@ func TestPostRequestsWithMethodPost(t *testing.T) {
Post("/").
Reply(http.StatusOK)
err := Post("http://localhost/", "", nil, nil)
err := Post("http://localhost/", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error %q", err)
@@ -192,7 +191,7 @@ func TestPutRequestsWithMethodPut(t *testing.T) {
Put("/").
Reply(http.StatusOK)
err := Put("http://localhost/", "", nil, nil)
err := Put("http://localhost/", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error %q", err)
@@ -209,7 +208,7 @@ func TestDeleteRequestsWithMethodDelete(t *testing.T) {
Delete("/").
Reply(http.StatusOK)
err := Delete("http://localhost/", "", nil, nil)
err := Delete("http://localhost/", nil, nil, nil)
if err != nil {
t.Errorf("Unexpected error %q", err)
-50
View File
@@ -58,56 +58,6 @@ func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason st
return errList
}
func TestConfigRouterHasRoutesWellConfigured(t *testing.T) {
testCases := []struct {
pattern, method string
handler uintptr
mustMatch bool
vars []string
}{
{"/routes/FOO", http.MethodGet, reflect.ValueOf(getRoute).Pointer(), true, []string{"id"}},
{"/routes/FOO", http.MethodPut, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}},
{"/routes/FOO", http.MethodPost, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}},
{"/routes/FOO", http.MethodDelete, reflect.ValueOf(removeRoute).Pointer(), true, []string{"id"}},
{"/routes", http.MethodGet, reflect.ValueOf(listRoutes).Pointer(), true, []string{}},
{"/routes", http.MethodPut, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}},
{"/routes", http.MethodPost, reflect.ValueOf(addRoute).Pointer(), true, []string{}},
{"/routes", http.MethodDelete, reflect.ValueOf(defMethodNotAllowedHandler).Pointer(), true, []string{}},
{"/", http.MethodGet, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/", http.MethodPut, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/", http.MethodPost, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/", http.MethodDelete, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/FOO", http.MethodGet, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/FOO", http.MethodPut, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/FOO", http.MethodPost, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
{"/FOO", http.MethodDelete, reflect.ValueOf(defNotFoundHandler).Pointer(), true, []string{}},
}
r := configRouter()
for _, tc := range testCases {
rm := mux.RouteMatch{}
rq, _ := http.NewRequest(tc.method, tc.pattern, nil)
if matched := r.Match(rq, &rm); tc.mustMatch == matched {
if tc.mustMatch {
// Check for Handler match.
realHandler := reflect.ValueOf(rm.Handler).Pointer()
if realHandler != tc.handler {
t.Errorf("Handler mismatch. Expected: %X, got: %X", tc.handler, realHandler)
}
// Check for variables
for _, vn := range tc.vars {
if _, exists := rm.Vars[vn]; !exists {
t.Errorf("Variable not present: %s", vn)
}
}
}
} else {
t.Errorf("Route mismatch: %+v", tc)
}
}
}
func TestPathValidatorNoErrorWhenCorrectPath(t *testing.T) {
err := pathValidator("/routes/{routeID}")
+26 -3
View File
@@ -17,24 +17,47 @@
package control
import (
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"sync"
"github.com/BBVA/kapow/internal/certs"
"github.com/BBVA/kapow/internal/logger"
)
// Run Starts the control server listening in bindAddr
func Run(bindAddr string, wg *sync.WaitGroup) {
func Run(bindAddr string, wg *sync.WaitGroup, serverCert, clientCert certs.Cert) {
listener, err := net.Listen("tcp", bindAddr)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(clientCert.SignedCertPEMBytes())
ln, err := net.Listen("tcp", bindAddr)
if err != nil {
logger.L.Fatal(err)
}
server := &http.Server{
Addr: bindAddr,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{
tls.Certificate{
Certificate: [][]byte{serverCert.SignedCert},
PrivateKey: serverCert.PrivKey,
Leaf: serverCert.X509Cert,
},
},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
},
Handler: configRouter(),
}
// Signal startup
logger.L.Printf("ControlServer listening at %s\n", bindAddr)
wg.Done()
logger.L.Fatal(http.Serve(listener, configRouter()))
// Listen to HTTPS connections with the server certificate and wait
logger.L.Fatal(server.ServeTLS(ln, "", ""))
}
+5 -1
View File
@@ -19,6 +19,7 @@ package server
import (
"sync"
"github.com/BBVA/kapow/internal/certs"
"github.com/BBVA/kapow/internal/server/control"
"github.com/BBVA/kapow/internal/server/data"
"github.com/BBVA/kapow/internal/server/user"
@@ -34,13 +35,16 @@ type ServerConfig struct {
ClientAuth,
Debug bool
ControlServerCert certs.Cert
ControlClientCert certs.Cert
}
// StartServer Starts one instance of each server in a goroutine and remains listening on a channel for trace events generated by them
func StartServer(config ServerConfig) {
var wg = sync.WaitGroup{}
wg.Add(3)
go control.Run(config.ControlBindAddr, &wg)
go control.Run(config.ControlBindAddr, &wg, config.ControlServerCert, config.ControlClientCert)
go data.Run(config.DataBindAddr, &wg)
go user.Run(config.UserBindAddr, &wg, config.CertFile, config.KeyFile, config.ClientCaFile, config.ClientAuth, config.Debug)