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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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, "", ""))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user