Commit 573514c6 authored by Kirill Smelkov's avatar Kirill Smelkov

X go/neo: Add SSL support

parent cc4854b9
...@@ -615,12 +615,56 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) ( ...@@ -615,12 +615,56 @@ func openClientByURL(ctx context.Context, u *url.URL, opt *zodb.DriverOptions) (
return nil, zodb.InvalidTid, fmt.Errorf("cluster name not specified") return nil, zodb.InvalidTid, fmt.Errorf("cluster name not specified")
} }
qv, err := url.ParseQuery(u.RawQuery)
if err != nil {
return nil, zodb.InvalidTid, err
}
q := map[string]string{}
for k, vv := range qv {
if len(vv) == 0 {
return nil, zodb.InvalidTid, fmt.Errorf("parameter %q without value", k)
}
if len(vv) != 1 {
return nil, zodb.InvalidTid, fmt.Errorf("duplicate parameter %q ", k)
}
q[k] = vv[0]
}
qpop := func(k string) string {
v := q[k]
delete(q, k)
return v
}
ssl := false
ca := qpop("ca")
cert := qpop("cert")
key := qpop("key")
if len(q) != 0 {
return nil, zodb.InvalidTid, fmt.Errorf("invalid query: %v", q)
}
if ca != "" || cert != "" || key != "" {
if !(ca != "" && cert != "" && key != "") {
return nil, zodb.InvalidTid, fmt.Errorf("incomplete ca/cert/key provided")
}
ssl = true
}
if !opt.ReadOnly { if !opt.ReadOnly {
return nil, zodb.InvalidTid, fmt.Errorf("TODO write mode not implemented") return nil, zodb.InvalidTid, fmt.Errorf("TODO write mode not implemented")
} }
// XXX check/use other url fields net := xnet.NetPlain("tcp") // TODO not only "tcp" ?
net := xnet.NetPlain("tcp") // TODO + TLS; not only "tcp" ? if ssl {
tlsCfg, err := tlsForSSL(ca, cert, key)
if err != nil {
return nil, zodb.InvalidTid, err
}
net = xnet.NetTLS(net, tlsCfg)
}
c := NewClient(u.User.Username(), u.Host, net) c := NewClient(u.User.Username(), u.Host, net)
c.watchq = opt.Watchq c.watchq = opt.Watchq
......
...@@ -26,6 +26,7 @@ import ( ...@@ -26,6 +26,7 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"strings"
"testing" "testing"
"time" "time"
...@@ -38,8 +39,10 @@ import ( ...@@ -38,8 +39,10 @@ import (
// NEOSrv represents running NEO server. // NEOSrv represents running NEO server.
type NEOSrv interface { type NEOSrv interface {
ClusterName() string // name of the cluster // XXX kill or restore?
MasterAddr() string // address of the master //ClusterName() string // name of the cluster
//MasterAddr() string // address of the master
ZUrl() string // zurl to access this NEO server
Bugs() []string // list of known server bugs Bugs() []string // list of known server bugs
} }
...@@ -57,6 +60,11 @@ type NEOPySrv struct { ...@@ -57,6 +60,11 @@ type NEOPySrv struct {
errExit error // error from Wait errExit error // error from Wait
masterAddr string // address of master in spawned cluster masterAddr string // address of master in spawned cluster
// CA/Cert/Key used for SSL exchange. Empty if SSL is not used
CA string
Cert string
Key string
} }
func (_ *NEOPySrv) Bugs() []string { func (_ *NEOPySrv) Bugs() []string {
...@@ -71,6 +79,8 @@ type NEOPyOptions struct { ...@@ -71,6 +79,8 @@ type NEOPyOptions struct {
// nreplica // nreplica
// name // name
SSL bool // whether to use SSL for node-node exchange
} }
// StartNEOPySrv starts NEO/py server for clusterName NEO database located in workdir/. // StartNEOPySrv starts NEO/py server for clusterName NEO database located in workdir/.
...@@ -90,8 +100,20 @@ func StartNEOPySrv(workdir, clusterName string, opt NEOPyOptions) (_ *NEOPySrv, ...@@ -90,8 +100,20 @@ func StartNEOPySrv(workdir, clusterName string, opt NEOPyOptions) (_ *NEOPySrv,
} }
n := &NEOPySrv{workdir: workdir, clusterName: clusterName, cancel: cancel, done: make(chan struct{})} n := &NEOPySrv{workdir: workdir, clusterName: clusterName, cancel: cancel, done: make(chan struct{})}
if opt.SSL {
npytests := "../../neo/tests/"
n.CA = npytests + "ca.crt"
n.Cert = npytests + "node.crt"
n.Key = npytests + "node.key"
}
// XXX $PYTHONPATH to top, so that `import neo` works? // XXX $PYTHONPATH to top, so that `import neo` works?
n.pysrv = xexec.Command("./py/runneo.py", workdir, clusterName) // XXX +opt n.pysrv = xexec.Command("./py/runneo.py", workdir, clusterName)
if opt.SSL {
n.pysrv.Args = append(n.pysrv.Args, "ca=" +n.CA)
n.pysrv.Args = append(n.pysrv.Args, "cert="+n.Cert)
n.pysrv.Args = append(n.pysrv.Args, "key=" +n.Key)
}
n.opt = opt n.opt = opt
// $TEMP -> workdir (else NEO/py creates another one for e.g. coverage) // $TEMP -> workdir (else NEO/py creates another one for e.g. coverage)
n.pysrv.Env = append(os.Environ(), "TEMP="+workdir) n.pysrv.Env = append(os.Environ(), "TEMP="+workdir)
...@@ -152,6 +174,23 @@ func (n *NEOPySrv) MasterAddr() string { ...@@ -152,6 +174,23 @@ func (n *NEOPySrv) MasterAddr() string {
return n.masterAddr return n.masterAddr
} }
func (n *NEOPySrv) ZUrl() string {
zurl := fmt.Sprintf("neo://%s@%s", n.ClusterName(), n.MasterAddr())
argv := []string{}
if n.opt.SSL {
argv = append(argv, "ca=" +url.QueryEscape(n.CA))
argv = append(argv, "cert="+url.QueryEscape(n.Cert))
argv = append(argv, "key=" +url.QueryEscape(n.Key))
}
if len(argv) != 0 {
zurl += "?"
zurl += strings.Join(argv, "&")
}
return zurl
}
func (n *NEOPySrv) Close() (err error) { func (n *NEOPySrv) Close() (err error) {
defer xerr.Contextf(&err, "stopneo %s", n.workdir) defer xerr.Contextf(&err, "stopneo %s", n.workdir)
...@@ -194,39 +233,54 @@ func withNEOSrv(t *testing.T, f func(t *testing.T, nsrv NEOSrv), optv ...tOption ...@@ -194,39 +233,54 @@ func withNEOSrv(t *testing.T, f func(t *testing.T, nsrv NEOSrv), optv ...tOption
f(work) f(work)
} }
// TODO + all variants with nreplic=X, npartition=Y, nmaster=Z, ... ? // TODO + all variants with nreplica=X, npartition=Y, nmaster=Z, ... ?
// NEO/py for _, ssl := range []bool{false, true} {
t.Run("py", func(t *testing.T) { kind := ""
t.Helper() if ssl { kind = "ssl" } else { kind = "!ssl" }
// XXX needpy
inWorkDir(t, func(workdir string) { // NEO/py
X := xtesting.FatalIf(t) t.Run("py/"+kind, func(t *testing.T) {
t.Helper()
npy, err := StartNEOPySrv(workdir, "1", NEOPyOptions{}); X(err) // XXX needpy
defer func() { inWorkDir(t, func(workdir string) {
err := npy.Close(); X(err) X := xtesting.FatalIf(t)
}()
npy, err := StartNEOPySrv(workdir, "1", NEOPyOptions{
if opt.Preload != "" { SSL: ssl,
cmd := exec.Command("python", "-c", }); X(err)
"from neo.scripts.neomigrate import main; main()", defer func() {
"-q", err := npy.Close(); X(err)
"-c", npy.ClusterName(), }()
opt.Preload,
npy.MasterAddr()) if opt.Preload != "" {
cmd.Stdin = nil cmd := exec.Command("python", "-c",
cmd.Stdout = os.Stdout "from neo.scripts.neomigrate import main; main()",
cmd.Stderr = os.Stderr "-q",
err := cmd.Run(); X(err) "-c", npy.ClusterName(),
} )
if ssl {
f(t, npy) cmd.Args = append(cmd.Args, "--ca", npy.CA)
cmd.Args = append(cmd.Args, "--cert", npy.Cert)
cmd.Args = append(cmd.Args, "--key", npy.Key)
}
cmd.Args = append(cmd.Args,
opt.Preload,
npy.MasterAddr(),
)
cmd.Stdin = nil
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run(); X(err)
}
f(t, npy)
})
}) })
})
// TODO NEO/go // TODO NEO/go
}
} }
// withNEO tests f on all kinds of NEO servers connected to by NEO client. // withNEO tests f on all kinds of NEO servers connected to by NEO client.
...@@ -235,7 +289,7 @@ func withNEO(t *testing.T, f func(t *testing.T, nsrv NEOSrv, ndrv *Client), optv ...@@ -235,7 +289,7 @@ func withNEO(t *testing.T, f func(t *testing.T, nsrv NEOSrv, ndrv *Client), optv
withNEOSrv(t, func(t *testing.T, nsrv NEOSrv) { withNEOSrv(t, func(t *testing.T, nsrv NEOSrv) {
t.Helper() t.Helper()
X := xtesting.FatalIf(t) X := xtesting.FatalIf(t)
ndrv, _, err := neoOpen(fmt.Sprintf("neo://%s@%s", nsrv.ClusterName(), nsrv.MasterAddr()), ndrv, _, err := neoOpen(nsrv.ZUrl(),
&zodb.DriverOptions{ReadOnly: true}); X(err) &zodb.DriverOptions{ReadOnly: true}); X(err)
defer func() { defer func() {
err := ndrv.Close(); X(err) err := ndrv.Close(); X(err)
...@@ -271,7 +325,7 @@ func TestLoad(t *testing.T) { ...@@ -271,7 +325,7 @@ func TestLoad(t *testing.T) {
func TestWatch(t *testing.T) { func TestWatch(t *testing.T) {
withNEOSrv(t, func(t *testing.T, nsrv NEOSrv) { withNEOSrv(t, func(t *testing.T, nsrv NEOSrv) {
xtesting.DrvTestWatch(t, fmt.Sprintf("neo://%s@%s", nsrv.ClusterName(), nsrv.MasterAddr()), openClientByURL) xtesting.DrvTestWatch(t, nsrv.ZUrl(), openClientByURL)
}) })
} }
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
# See https://www.nexedi.com/licensing for rationale and options. # See https://www.nexedi.com/licensing for rationale and options.
"""runneo.py runs NEO/py cluster for NEO/go testing. """runneo.py runs NEO/py cluster for NEO/go testing.
Usage: runneo.py <workdir> <cluster-name> XXX + (**kw for NEOCluster) Usage: runneo.py <workdir> <cluster-name> [k1=v1] [k2=v2] ...
<workdir>/ready is created with address of master after spawned cluster becomes <workdir>/ready is created with address of master after spawned cluster becomes
operational. operational.
...@@ -40,7 +40,14 @@ def main(): ...@@ -40,7 +40,14 @@ def main():
clusterName = sys.argv[2] clusterName = sys.argv[2]
readyf = workdir + "/ready" readyf = workdir + "/ready"
def sinfo(msg): return "I: runneo.py: %s/%s: %s" % (workdir, clusterName, msg) kw = {}
for arg in sys.argv[3:]:
k, v = arg.split('=')
kw[k] = v
flags = ''
def sinfo(msg): return "I: runneo.py: %s/%s%s: %s" % (workdir, clusterName, flags, msg)
def info(msg): print(sinfo(msg)) def info(msg): print(sinfo(msg))
# SIGTERM -> exit gracefully, so that defers are run # SIGTERM -> exit gracefully, so that defers are run
...@@ -48,7 +55,21 @@ def main(): ...@@ -48,7 +55,21 @@ def main():
raise SystemExit(sinfo("terminated")) raise SystemExit(sinfo("terminated"))
signal(SIGTERM, _) signal(SIGTERM, _)
cluster = NEOCluster([clusterName], adapter='SQLite', name=clusterName, temp_dir=workdir) # XXX +kw flags = ' !ssl'
ca = kw.pop('ca', None)
cert = kw.pop('cert', None)
key = kw.pop('key', None)
if ca or cert or key:
if not (ca and cert and key):
raise RuntimeError(sinfo("incomplete ca/cert/key provided"))
# neo/py does `NEOCluster.SSL = neo.test.SSL` (= (ca.crt, node.crt, node.key) )
flags = ' ssl'
NEOCluster.SSL = (ca, cert, key)
if kw:
raise RuntimeError(sinfo("unexpected flags: %s" % kw))
cluster = NEOCluster([clusterName], adapter='SQLite', name=clusterName, temp_dir=workdir)
cluster.start() cluster.start()
defer(cluster.stop) defer(cluster.stop)
......
// Copyright (C) 2017 Nexedi SA and Contributors. // Copyright (C) 2017-2020 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com> // Kirill Smelkov <kirr@nexedi.com>
// //
// This program is free software: you can Use, Study, Modify and Redistribute // This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your // it under the terms of the GNU General Public License version 3, or (at your
...@@ -21,7 +21,13 @@ package neo ...@@ -21,7 +21,13 @@ package neo
import ( import (
"context" "context"
"crypto/tls"
"crypto/x509"
"fmt"
"io" "io"
"io/ioutil"
"lab.nexedi.com/kirr/go123/xerr"
"lab.nexedi.com/kirr/neo/go/zodb" "lab.nexedi.com/kirr/neo/go/zodb"
"lab.nexedi.com/kirr/neo/go/internal/log" "lab.nexedi.com/kirr/neo/go/internal/log"
...@@ -64,3 +70,82 @@ func before2At(before zodb.Tid) (at zodb.Tid) { ...@@ -64,3 +70,82 @@ func before2At(before zodb.Tid) (at zodb.Tid) {
return zodb.TidMax return zodb.TidMax
} }
} }
// tlsForSSL builds tls.Config from ca/cert/key files that should be interoperable with NEO/py.
//
// see https://lab.nexedi.com/nexedi/neoppod/blob/v1.12-61-gc1c26894/neo/lib/app.py#L74-90
func tlsForSSL(ca, cert, key string) (_ *tls.Config, err error) {
defer xerr.Contextf(&err, "tls setup")
caData, err := ioutil.ReadFile(ca)
if err != nil {
return nil, err
}
CA := x509.NewCertPool()
ok := CA.AppendCertsFromPEM(caData)
if !ok {
return nil, fmt.Errorf("invalid CA")
}
crt, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, err
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{crt}, // (cert, key) as loaded
RootCAs: CA, // (ca,) as loaded
// a server also verifies cient (but also see verifyPeerCert below)
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: CA,
PreferServerCipherSuites: true,
}
// TODO only accept TLS >= 1.2 ?
// tls docs say we should parse Certificate[0] into Leaf ourselves
leaf, err := x509.ParseCertificate(crt.Certificate[0])
if err != nil {
return nil, err
}
crt.Leaf = leaf
// NEO/py does not verify CommonName (ssl.check_hostname=False implicitly).
// Match that behaviour with custom VerifyPeerCertificate because Go
// does not provide functionality to skip only CN verification out of the box.
// https://github.com/golang/go/issues/21971#issuecomment-332693931
// https://stackoverflow.com/questions/44295820
verifyPeerCert := func(rawCerts [][]byte, _ [][]*x509.Certificate) (err error) {
defer xerr.Contextf(&err, "verify peer cert")
certv := []*x509.Certificate{}
for _, certData := range rawCerts {
cert, err := x509.ParseCertificate(certData)
if err != nil {
return err
}
certv = append(certv, cert)
}
vopt := x509.VerifyOptions{
DNSName: "", // means "don't verify name"
Roots: tlsCfg.RootCAs,
Intermediates: x509.NewCertPool(),
}
for _, cert := range certv[1:] {
vopt.Intermediates.AddCert(cert)
}
_, err = certv[0].Verify(vopt)
return err
}
tlsCfg.InsecureSkipVerify = true // disables all verifications including for ServerName
tlsCfg.VerifyPeerCertificate = verifyPeerCert
return tlsCfg, nil
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment