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:
@@ -62,10 +62,9 @@ You can find the complete documentation and examples [here](https://kapow.readth
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Please consider the following security caveats **before** using *Kapow!*
|
Please consider the following
|
||||||
|
[Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns)
|
||||||
- [Issue #119](https://github.com/BBVA/kapow/issues/119)
|
**before** using *Kapow!*
|
||||||
- [Security Concerns](https://kapow.readthedocs.io/en/stable/the_project/security.html#security-concerns)
|
|
||||||
|
|
||||||
If you are not 100% sure about what you are doing we recommend not using *Kapow!*
|
If you are not 100% sure about what you are doing we recommend not using *Kapow!*
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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
|
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:
|
.. _http-data-interface:
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ The spawned entrypoint is run with the following variables added to its
|
|||||||
environment:
|
environment:
|
||||||
|
|
||||||
- :envvar:`KAPOW_HANDLER_ID`: Containing the `HANDLER_ID`
|
- :envvar:`KAPOW_HANDLER_ID`: Containing the `HANDLER_ID`
|
||||||
- :envvar:`KAPOW_DATAAPI_URL`: With the URL of the :ref:`http-data-interface`
|
- :envvar:`KAPOW_DATA_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_CONTROL_URL`: With the URL of the :ref:`https-control-interface`
|
||||||
|
|
||||||
|
|
||||||
3. ``kapow set /response/body banana``
|
3. ``kapow set /response/body banana``
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ Or, if you want human-readable output, you can use :program:`jq`:
|
|||||||
|
|
||||||
.. note::
|
.. 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``.
|
``localhost:8081``.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// GetData will perform the request and write the results on the provided writer
|
||||||
func GetData(host, id, path string, w io.Writer) error {
|
func GetData(host, id, path string, w io.Writer) error {
|
||||||
url := host + "/handlers/" + id + path
|
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
|
payload["entrypoint"] = entrypoint
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(payload)
|
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
|
// ListRoutes queries the kapow! instance for the routes that are registered
|
||||||
func ListRoutes(host string, w io.Writer) error {
|
func ListRoutes(host string, w io.Writer) error {
|
||||||
url := host + "/routes"
|
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
|
// RemoveRoute removes a registered route in Kapow! server
|
||||||
func RemoveRoute(host, id string) error {
|
func RemoveRoute(host, id string) error {
|
||||||
url := host + "/routes/" + id
|
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 {
|
func SetData(host, handlerID, path string, r io.Reader) error {
|
||||||
url := host + "/handlers/" + handlerID + path
|
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)
|
// TODO: Manage args for url_pattern and command_file (2 exact args)
|
||||||
var routeAddCmd = &cobra.Command{
|
var routeAddCmd = &cobra.Command{
|
||||||
@@ -78,7 +78,7 @@ func init() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
// TODO: Add default values for flags and remove path flag
|
// 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("method", "X", "GET", "HTTP method to accept")
|
||||||
routeAddCmd.Flags().StringP("entrypoint", "e", "", "Command to execute")
|
routeAddCmd.Flags().StringP("entrypoint", "e", "", "Command to execute")
|
||||||
routeAddCmd.Flags().StringP("command", "c", "", "Command to pass to the shell")
|
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(routeListCmd)
|
||||||
RouteCmd.AddCommand(routeAddCmd)
|
RouteCmd.AddCommand(routeAddCmd)
|
||||||
|
|||||||
+60
-3
@@ -19,16 +19,40 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/BBVA/kapow/internal/certs"
|
||||||
"github.com/BBVA/kapow/internal/logger"
|
"github.com/BBVA/kapow/internal/logger"
|
||||||
"github.com/BBVA/kapow/internal/server"
|
"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
|
// ServerCmd is the command line interface for kapow server
|
||||||
var ServerCmd = &cobra.Command{
|
var ServerCmd = &cobra.Command{
|
||||||
Use: "server [optional flags] [optional init program(s)]",
|
Use: "server [optional flags] [optional init program(s)]",
|
||||||
@@ -42,6 +66,8 @@ var ServerCmd = &cobra.Command{
|
|||||||
sConf.ControlBindAddr, _ = cmd.Flags().GetString("control-bind")
|
sConf.ControlBindAddr, _ = cmd.Flags().GetString("control-bind")
|
||||||
sConf.DataBindAddr, _ = cmd.Flags().GetString("data-bind")
|
sConf.DataBindAddr, _ = cmd.Flags().GetString("data-bind")
|
||||||
|
|
||||||
|
controlReachableAddr, _ := cmd.Flags().GetString("control-reachable-addr")
|
||||||
|
|
||||||
sConf.CertFile, _ = cmd.Flags().GetString("certfile")
|
sConf.CertFile, _ = cmd.Flags().GetString("certfile")
|
||||||
sConf.KeyFile, _ = cmd.Flags().GetString("keyfile")
|
sConf.KeyFile, _ = cmd.Flags().GetString("keyfile")
|
||||||
|
|
||||||
@@ -49,29 +75,51 @@ var ServerCmd = &cobra.Command{
|
|||||||
sConf.ClientCaFile, _ = cmd.Flags().GetString("clientcafile")
|
sConf.ClientCaFile, _ = cmd.Flags().GetString("clientcafile")
|
||||||
sConf.Debug, _ = cmd.Flags().GetBool("debug")
|
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
|
// 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 {
|
if _, exist := os.LookupEnv("KAPOW_DATA_URL"); !exist {
|
||||||
os.Setenv("KAPOW_DATA_URL", "http://"+sConf.DataBindAddr)
|
os.Setenv("KAPOW_DATA_URL", "http://"+sConf.DataBindAddr)
|
||||||
}
|
}
|
||||||
if _, exist := os.LookupEnv("KAPOW_CONTROL_URL"); !exist {
|
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)
|
server.StartServer(sConf)
|
||||||
|
|
||||||
for _, path := range args {
|
for _, path := range args {
|
||||||
go Run(path, sConf.Debug)
|
go Run(
|
||||||
|
path,
|
||||||
|
sConf.Debug,
|
||||||
|
sConf.ControlServerCert.SignedCertPEMBytes(),
|
||||||
|
sConf.ControlClientCert.SignedCertPEMBytes(),
|
||||||
|
sConf.ControlClientCert.PrivateKeyPEMBytes(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {}
|
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() {
|
func init() {
|
||||||
ServerCmd.Flags().String("bind", "0.0.0.0:8080", "IP address and port to bind the user interface to")
|
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("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("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("certfile", "", "Cert file to serve thru https")
|
||||||
ServerCmd.Flags().String("keyfile", "", "Key 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
|
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)
|
logger.L.Printf("Running init program %+q", path)
|
||||||
cmd := BuildCmd(path)
|
cmd := BuildCmd(path)
|
||||||
cmd.Env = os.Environ()
|
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
|
var wg sync.WaitGroup
|
||||||
if debug {
|
if debug {
|
||||||
|
|||||||
+70
-12
@@ -17,30 +17,41 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"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
|
// Get perform a request using Request with the GET method
|
||||||
func Get(url string, contentType string, r io.Reader, w io.Writer) error {
|
func Get(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
|
||||||
return Request("GET", url, contentType, r, w)
|
return Request("GET", url, r, w, clientGenerator, reqTuner...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post perform a request using Request with the POST method
|
// Post perform a request using Request with the POST method
|
||||||
func Post(url string, contentType string, r io.Reader, w io.Writer) error {
|
func Post(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
|
||||||
return Request("POST", url, contentType, r, w)
|
return Request("POST", url, r, w, clientGenerator, reqTuner...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put perform a request using Request with the PUT method
|
// Put perform a request using Request with the PUT method
|
||||||
func Put(url string, contentType string, r io.Reader, w io.Writer) error {
|
func Put(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
|
||||||
return Request("PUT", url, contentType, r, w)
|
return Request("PUT", url, r, w, clientGenerator, reqTuner...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete perform a request using Request with the DELETE method
|
// Delete perform a request using Request with the DELETE method
|
||||||
func Delete(url string, contentType string, r io.Reader, w io.Writer) error {
|
func Delete(url string, r io.Reader, w io.Writer, clientGenerator func() *http.Client, reqTuner ...func(*http.Request)) error {
|
||||||
return Request("DELETE", url, contentType, r, w)
|
return Request("DELETE", url, r, w, clientGenerator, reqTuner...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var devnull = ioutil.Discard
|
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
|
// 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
|
// of the response to the given writer. The reader and writer are
|
||||||
// optional.
|
// 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)
|
req, err := http.NewRequest(method, url, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentType != "" {
|
for _, reqTuner := range reqTuners {
|
||||||
req.Header.Add("Content-Type", contentType)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -81,3 +99,43 @@ func Request(method string, url string, contentType string, r io.Reader, w io.Wr
|
|||||||
|
|
||||||
return err
|
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()
|
defer gock.Off()
|
||||||
gock.New("").Reply(200)
|
gock.New("").Reply(200)
|
||||||
|
|
||||||
err := Request("GET", "://", "", nil, nil)
|
err := Request("GET", "://", nil, nil, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error not returned")
|
t.Errorf("Expected error not returned")
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ func TestRequestGivenMethod(t *testing.T) {
|
|||||||
mock.Method = "FOO"
|
mock.Method = "FOO"
|
||||||
mock.Reply(200)
|
mock.Reply(200)
|
||||||
|
|
||||||
err := Request("FOO", "http://localhost", "", nil, nil)
|
err := Request("FOO", "http://localhost", nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error on request")
|
t.Errorf("Unexpected error on request")
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func TestReturnHTTPErrorAsIs(t *testing.T) {
|
|||||||
customError := errors.New("FOO")
|
customError := errors.New("FOO")
|
||||||
gock.New("http://localhost").ReplyError(customError)
|
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 {
|
if errors.Unwrap(err) != customError {
|
||||||
t.Errorf("Returned error is not the expected error: '%v'", err)
|
t.Errorf("Returned error is not the expected error: '%v'", err)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func TestReturnHTTPReasonAsErrorWhenUnsuccessful(t *testing.T) {
|
|||||||
Reply(http.StatusTeapot).
|
Reply(http.StatusTeapot).
|
||||||
BodyString(`{"reason": "I'm a teapot"}`)
|
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) {
|
if err == nil || err.Error() != http.StatusText(http.StatusTeapot) {
|
||||||
t.Errorf("Reason should be returned as an error")
|
t.Errorf("Reason should be returned as an error")
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func TestCopyResponseBodyToWriter(t *testing.T) {
|
|||||||
|
|
||||||
rw := new(bytes.Buffer)
|
rw := new(bytes.Buffer)
|
||||||
|
|
||||||
err := Request("GET", "http://localhost", "", nil, rw)
|
err := Request("GET", "http://localhost", nil, rw, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %v", err)
|
t.Errorf("Unexpected error %v", err)
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ func TestWriteToDevNullWhenNoWriter(t *testing.T) {
|
|||||||
|
|
||||||
defer func() { devnull = original }()
|
defer func() { devnull = original }()
|
||||||
|
|
||||||
err := Request("GET", "http://localhost", "", nil, nil)
|
err := Request("GET", "http://localhost", nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %v", err)
|
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()
|
defer gock.Off()
|
||||||
gock.New("http://localhost").
|
gock.New("http://localhost").
|
||||||
MatchHeader("Content-Type", "foo/bar").
|
MatchHeader("Content-Type", "application/json").
|
||||||
HeaderPresent("Content-Type").
|
|
||||||
Reply(http.StatusOK)
|
Reply(http.StatusOK)
|
||||||
|
|
||||||
err := Request("GET", "http://localhost", "foo/bar", nil, nil)
|
err := Request("GET", "http://localhost", nil, nil, nil, AsJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error '%v'", err.Error())
|
t.Errorf("Unexpected error '%v'", err.Error())
|
||||||
}
|
}
|
||||||
@@ -158,7 +157,7 @@ func TestGetRequestsWithMethodGet(t *testing.T) {
|
|||||||
Get("/").
|
Get("/").
|
||||||
Reply(http.StatusOK)
|
Reply(http.StatusOK)
|
||||||
|
|
||||||
err := Get("http://localhost/", "", nil, nil)
|
err := Get("http://localhost/", nil, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %q", err)
|
t.Errorf("Unexpected error %q", err)
|
||||||
@@ -175,7 +174,7 @@ func TestPostRequestsWithMethodPost(t *testing.T) {
|
|||||||
Post("/").
|
Post("/").
|
||||||
Reply(http.StatusOK)
|
Reply(http.StatusOK)
|
||||||
|
|
||||||
err := Post("http://localhost/", "", nil, nil)
|
err := Post("http://localhost/", nil, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %q", err)
|
t.Errorf("Unexpected error %q", err)
|
||||||
@@ -192,7 +191,7 @@ func TestPutRequestsWithMethodPut(t *testing.T) {
|
|||||||
Put("/").
|
Put("/").
|
||||||
Reply(http.StatusOK)
|
Reply(http.StatusOK)
|
||||||
|
|
||||||
err := Put("http://localhost/", "", nil, nil)
|
err := Put("http://localhost/", nil, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %q", err)
|
t.Errorf("Unexpected error %q", err)
|
||||||
@@ -209,7 +208,7 @@ func TestDeleteRequestsWithMethodDelete(t *testing.T) {
|
|||||||
Delete("/").
|
Delete("/").
|
||||||
Reply(http.StatusOK)
|
Reply(http.StatusOK)
|
||||||
|
|
||||||
err := Delete("http://localhost/", "", nil, nil)
|
err := Delete("http://localhost/", nil, nil, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error %q", err)
|
t.Errorf("Unexpected error %q", err)
|
||||||
|
|||||||
@@ -58,56 +58,6 @@ func checkErrorResponse(r *http.Response, expectedErrcode int, expectedReason st
|
|||||||
return errList
|
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) {
|
func TestPathValidatorNoErrorWhenCorrectPath(t *testing.T) {
|
||||||
err := pathValidator("/routes/{routeID}")
|
err := pathValidator("/routes/{routeID}")
|
||||||
|
|
||||||
|
|||||||
@@ -17,24 +17,47 @@
|
|||||||
package control
|
package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/BBVA/kapow/internal/certs"
|
||||||
"github.com/BBVA/kapow/internal/logger"
|
"github.com/BBVA/kapow/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run Starts the control server listening in bindAddr
|
// 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 {
|
if err != nil {
|
||||||
logger.L.Fatal(err)
|
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
|
// Signal startup
|
||||||
logger.L.Printf("ControlServer listening at %s\n", bindAddr)
|
logger.L.Printf("ControlServer listening at %s\n", bindAddr)
|
||||||
wg.Done()
|
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 (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/BBVA/kapow/internal/certs"
|
||||||
"github.com/BBVA/kapow/internal/server/control"
|
"github.com/BBVA/kapow/internal/server/control"
|
||||||
"github.com/BBVA/kapow/internal/server/data"
|
"github.com/BBVA/kapow/internal/server/data"
|
||||||
"github.com/BBVA/kapow/internal/server/user"
|
"github.com/BBVA/kapow/internal/server/user"
|
||||||
@@ -34,13 +35,16 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
ClientAuth,
|
ClientAuth,
|
||||||
Debug bool
|
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
|
// 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) {
|
func StartServer(config ServerConfig) {
|
||||||
var wg = sync.WaitGroup{}
|
var wg = sync.WaitGroup{}
|
||||||
wg.Add(3)
|
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 data.Run(config.DataBindAddr, &wg)
|
||||||
go user.Run(config.UserBindAddr, &wg, config.CertFile, config.KeyFile, config.ClientCaFile, config.ClientAuth, config.Debug)
|
go user.Run(config.UserBindAddr, &wg, config.CertFile, config.KeyFile, config.ClientCaFile, config.ClientAuth, config.Debug)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user