package cmd import ( "crypto/tls" "fmt" "net" "context" "github.com/jinzhu/copier" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/problame/go-netssh" "github.com/problame/go-streamrpc" "github.com/zrepl/zrepl/cmd/tlsconf" "time" ) type SSHStdinserverConnecter struct { Host string User string Port uint16 IdentityFile string `mapstructure:"identity_file"` TransportOpenCommand []string `mapstructure:"transport_open_command"` SSHCommand string `mapstructure:"ssh_command"` Options []string DialTimeout string `mapstructure:"dial_timeout"` dialTimeout time.Duration } var _ streamrpc.Connecter = &SSHStdinserverConnecter{} func parseSSHStdinserverConnecter(i map[string]interface{}) (c *SSHStdinserverConnecter, err error) { c = &SSHStdinserverConnecter{} if err = mapstructure.Decode(i, c); err != nil { err = errors.New(fmt.Sprintf("could not parse ssh transport: %s", err)) return nil, err } if c.DialTimeout != "" { c.dialTimeout, err = time.ParseDuration(c.DialTimeout) if err != nil { return nil, errors.Wrap(err, "cannot parse dial_timeout") } } else { c.dialTimeout = 10 * time.Second } // TODO assert fields are filled return } type netsshConnToConn struct{ *netssh.SSHConn } var _ net.Conn = netsshConnToConn{} func (netsshConnToConn) SetDeadline(dl time.Time) error { return nil } func (netsshConnToConn) SetReadDeadline(dl time.Time) error { return nil } func (netsshConnToConn) SetWriteDeadline(dl time.Time) error { return nil } func (c *SSHStdinserverConnecter) Connect(dialCtx context.Context) (net.Conn, error) { var endpoint netssh.Endpoint if err := copier.Copy(&endpoint, c); err != nil { return nil, errors.WithStack(err) } dialCtx, dialCancel := context.WithTimeout(dialCtx, c.dialTimeout) // context.TODO tied to error handling below defer dialCancel() nconn, err := netssh.Dial(dialCtx, endpoint) if err != nil { if err == context.DeadlineExceeded { err = errors.Errorf("dial_timeout of %s exceeded", c.dialTimeout) } return nil, err } return netsshConnToConn{nconn}, nil } type TCPConnecter struct { Host string Port uint16 dialer net.Dialer tlsConfig *tls.Config } func parseTCPConnecter(i map[string]interface{}) (*TCPConnecter, error) { var in struct { Host string Port uint16 DialTimeout string `mapstructure:"dial_timeout"` TLS map[string]interface{} } if err := mapstructure.Decode(i, &in); err != nil { return nil, errors.Wrap(err, "mapstructure error") } if in.Host == "" || in.Port == 0 { return nil, errors.New("fields 'host' and 'port' must not be empty") } dialTimeout, err := parsePostitiveDuration(in.DialTimeout) if err != nil { if in.DialTimeout != "" { return nil, errors.Wrap(err, "cannot parse field 'dial_timeout'") } dialTimeout = 10 * time.Second } dialer := net.Dialer{ Timeout: dialTimeout, } var tlsConfig *tls.Config if in.TLS != nil { tlsConfig, err = func(i map[string]interface{}) (config *tls.Config, err error) { var in struct { CA string Cert string Key string ServerCN string `mapstructure:"server_cn"` } if err := mapstructure.Decode(i, &in); err != nil { return nil, errors.Wrap(err, "mapstructure error") } if in.CA == "" || in.Cert == "" || in.Key == "" || in.ServerCN == "" { return nil, errors.New("fields 'ca', 'cert', 'key' and 'server_cn' must be specified") } ca, err := tlsconf.ParseCAFile(in.CA) if err != nil { return nil, errors.Wrap(err, "cannot parse ca file") } cert, err := tls.LoadX509KeyPair(in.Cert, in.Key) if err != nil { return nil, errors.Wrap(err, "cannot parse cert/key pair") } return tlsconf.ClientAuthClient(in.ServerCN, ca, cert) }(in.TLS) if err != nil { return nil, errors.Wrap(err, "cannot parse TLS config in field 'tls'") } } return &TCPConnecter{in.Host, in.Port, dialer, tlsConfig}, nil } func (c *TCPConnecter) Connect(dialCtx context.Context) (conn net.Conn, err error) { addr := fmt.Sprintf("%s:%d", c.Host, c.Port) if c.tlsConfig != nil { return tls.DialWithDialer(&c.dialer, "tcp", addr, c.tlsConfig) } return c.dialer.DialContext(dialCtx, "tcp", addr) }