Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81e37f8812 |
@@ -889,6 +889,41 @@ func (i *jsIPN) listen(network, addr string) js.Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tlsClientConfigFromJS builds a client tls.Config from optional JS options
|
||||||
|
// (serverName, insecureSkipVerify, caCerts). defaultServerName may be empty
|
||||||
|
// (STARTTLS upgrade case), in which case serverName must be provided unless
|
||||||
|
// insecureSkipVerify is set.
|
||||||
|
//
|
||||||
|
// On wasm there's no system root pool, so default to the baked-in
|
||||||
|
// LetsEncrypt roots (which is what `tailscale cert` uses for tailnet
|
||||||
|
// HTTPS endpoints). Callers can override with caCerts (PEM) or bypass
|
||||||
|
// entirely with insecureSkipVerify.
|
||||||
|
func tlsClientConfigFromJS(defaultServerName string, opts js.Value) (*tls.Config, error) {
|
||||||
|
cfg := &tls.Config{
|
||||||
|
ServerName: defaultServerName,
|
||||||
|
RootCAs: bakedroots.Get(),
|
||||||
|
}
|
||||||
|
if !opts.IsUndefined() && !opts.IsNull() {
|
||||||
|
if sn := opts.Get("serverName"); sn.Type() == js.TypeString {
|
||||||
|
cfg.ServerName = sn.String()
|
||||||
|
}
|
||||||
|
if iv := opts.Get("insecureSkipVerify"); iv.Type() == js.TypeBoolean {
|
||||||
|
cfg.InsecureSkipVerify = iv.Bool()
|
||||||
|
}
|
||||||
|
if ca := opts.Get("caCerts"); ca.Type() == js.TypeString {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if !pool.AppendCertsFromPEM([]byte(ca.String())) {
|
||||||
|
return nil, fmt.Errorf("caCerts: no valid PEM certificates found")
|
||||||
|
}
|
||||||
|
cfg.RootCAs = pool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.ServerName == "" && !cfg.InsecureSkipVerify {
|
||||||
|
return nil, fmt.Errorf("serverName is required unless insecureSkipVerify is set")
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *jsIPN) dialTLS(addr string, opts js.Value) js.Value {
|
func (i *jsIPN) dialTLS(addr string, opts js.Value) js.Value {
|
||||||
return makePromise(func() (any, error) {
|
return makePromise(func() (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
@@ -899,28 +934,9 @@ func (i *jsIPN) dialTLS(addr string, opts js.Value) js.Value {
|
|||||||
return nil, fmt.Errorf("invalid address %q: %w", addr, err)
|
return nil, fmt.Errorf("invalid address %q: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On wasm there's no system root pool, so default to the
|
cfg, err := tlsClientConfigFromJS(host, opts)
|
||||||
// baked-in LetsEncrypt roots (which is what `tailscale cert`
|
if err != nil {
|
||||||
// uses for tailnet HTTPS endpoints). Callers can override with
|
return nil, err
|
||||||
// caCerts (PEM) or bypass entirely with insecureSkipVerify.
|
|
||||||
cfg := &tls.Config{
|
|
||||||
ServerName: host,
|
|
||||||
RootCAs: bakedroots.Get(),
|
|
||||||
}
|
|
||||||
if !opts.IsUndefined() && !opts.IsNull() {
|
|
||||||
if sn := opts.Get("serverName"); sn.Type() == js.TypeString {
|
|
||||||
cfg.ServerName = sn.String()
|
|
||||||
}
|
|
||||||
if iv := opts.Get("insecureSkipVerify"); iv.Type() == js.TypeBoolean {
|
|
||||||
cfg.InsecureSkipVerify = iv.Bool()
|
|
||||||
}
|
|
||||||
if ca := opts.Get("caCerts"); ca.Type() == js.TypeString {
|
|
||||||
pool := x509.NewCertPool()
|
|
||||||
if !pool.AppendCertsFromPEM([]byte(ca.String())) {
|
|
||||||
return nil, fmt.Errorf("caCerts: no valid PEM certificates found")
|
|
||||||
}
|
|
||||||
cfg.RootCAs = pool
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rawConn, err := i.dialer.UserDial(ctx, "tcp", addr)
|
rawConn, err := i.dialer.UserDial(ctx, "tcp", addr)
|
||||||
@@ -1464,6 +1480,46 @@ func wrapConn(conn net.Conn) map[string]any {
|
|||||||
"remoteAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
"remoteAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
return conn.RemoteAddr().String()
|
return conn.RemoteAddr().String()
|
||||||
}),
|
}),
|
||||||
|
// upgradeTLS wraps the conn in TLS in place (STARTTLS-style) and
|
||||||
|
// returns a new wrapped conn sharing the same underlying net.Conn;
|
||||||
|
// the old handle must not be used afterward. On handshake failure
|
||||||
|
// the underlying conn is closed. With isServer, certPem and keyPem
|
||||||
|
// are required; otherwise client options as in dialTLS apply.
|
||||||
|
"upgradeTLS": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
opts := js.Undefined()
|
||||||
|
if len(args) > 0 {
|
||||||
|
opts = args[0]
|
||||||
|
}
|
||||||
|
return makePromise(func() (any, error) {
|
||||||
|
var tlsConn *tls.Conn
|
||||||
|
hasOpts := !opts.IsUndefined() && !opts.IsNull()
|
||||||
|
if hasOpts && opts.Get("isServer").Type() == js.TypeBoolean && opts.Get("isServer").Bool() {
|
||||||
|
certPem := opts.Get("certPem")
|
||||||
|
keyPem := opts.Get("keyPem")
|
||||||
|
if certPem.Type() != js.TypeString || keyPem.Type() != js.TypeString {
|
||||||
|
return nil, fmt.Errorf("upgradeTLS: certPem and keyPem are required when isServer is set")
|
||||||
|
}
|
||||||
|
cert, err := tls.X509KeyPair([]byte(certPem.String()), []byte(keyPem.String()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("upgradeTLS: parsing cert/key: %w", err)
|
||||||
|
}
|
||||||
|
tlsConn = tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||||
|
} else {
|
||||||
|
cfg, err := tlsClientConfigFromJS("", opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn = tls.Client(conn, cfg)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wrapConn(tlsConn), nil
|
||||||
|
})
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user