package relayclient
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/coder/websocket"
"github.com/tmc/go-iroh/internal/relayproto"
"github.com/tmc/go-iroh/key"
"github.com/tmc/go-iroh/netaddr"
)
const relayPath = "/relay"
const maxFrameSize = 1024 * 1024
var (
ErrBadVersionHeader = errors .New ("relayclient: relay returned an unsupported protocol version" )
ErrHandshake = errors .New ("relayclient: handshake failed" )
)
type Options struct {
SecretKey key .SecretKey
TLSConfig *tls .Config
HTTPClient *http .Client
AuthToken string
}
type Client struct {
conn *websocket .Conn
version relayproto .ProtocolVersion
url netaddr .RelayURL
}
func Connect (ctx context .Context , u netaddr .RelayURL , opts Options ) (*Client , error ) {
dialURL , err := websocketURL (u )
if err != nil {
return nil , err
}
header := http .Header {}
if opts .AuthToken != "" {
header .Set ("Authorization" , "Bearer " +opts .AuthToken )
}
httpClient := opts .HTTPClient
if httpClient == nil {
httpClient = keyMaterialHTTPClient (opts .SecretKey , opts .TLSConfig )
}
conn , resp , err := websocket .Dial (ctx , dialURL , dialOptions (httpClient , header ))
if err != nil {
return nil , fmt .Errorf ("relayclient: dial %s: %w" , dialURL , err )
}
conn .SetReadLimit (maxFrameSize )
version , ok := relayproto .ParseProtocolVersion (resp .Header .Get ("Sec-WebSocket-Protocol" ))
if !ok {
version , ok = relayproto .ParseProtocolVersion (conn .Subprotocol ())
}
if !ok {
conn .Close (websocket .StatusProtocolError , "bad version" )
return nil , fmt .Errorf ("%w: %q" , ErrBadVersionHeader , resp .Header .Get ("Sec-WebSocket-Protocol" ))
}
c := &Client {conn : conn , version : version , url : u }
if err := c .handshake (ctx , opts .SecretKey ); err != nil {
conn .Close (websocket .StatusInternalError , "handshake failed" )
return nil , err
}
return c , nil
}
func keyMaterialHTTPClient(sk key .SecretKey , config *tls .Config ) *http .Client {
return &http .Client {Transport : keyMaterialTransport {secretKey : sk , tlsConfig : config }}
}
type keyMaterialTransport struct {
secretKey key .SecretKey
tlsConfig *tls .Config
}
func (t keyMaterialTransport ) RoundTrip (req *http .Request ) (*http .Response , error ) {
req = req .Clone (req .Context ())
tr := &http .Transport {TLSClientConfig : t .tlsConfig }
tr .DialTLSContext = func (ctx context .Context , network , addr string ) (net .Conn , error ) {
tlsConfig := t .tlsConfig
if tlsConfig == nil {
tlsConfig = &tls .Config {}
} else {
tlsConfig = tlsConfig .Clone ()
}
if tlsConfig .ServerName == "" {
host , _ , err := net .SplitHostPort (addr )
if err != nil {
host = addr
}
tlsConfig .ServerName = host
}
dialer := tls .Dialer {Config : tlsConfig }
conn , err := dialer .DialContext (ctx , network , addr )
if err != nil {
return nil , err
}
tlsConn , ok := conn .(*tls .Conn )
if !ok {
return conn , nil
}
state := tlsConn .ConnectionState ()
if auth , err := relayproto .NewKeyMaterialClientAuth (t .secretKey , &state ); err == nil {
if value , err := auth .HeaderValue (); err == nil {
req .Header .Set (relayproto .ClientAuthHeader , value )
}
}
return tlsConn , nil
}
return tr .RoundTrip (req )
}
func (c *Client ) Version () relayproto .ProtocolVersion { return c .version }
func (c *Client ) Send (ctx context .Context , msg relayproto .ClientToRelayMsg ) error {
return c .conn .Write (ctx , websocket .MessageBinary , msg .AppendTo (nil ))
}
func (c *Client ) Recv (ctx context .Context ) (relayproto .RelayToClientMsg , error ) {
_ , data , err := c .conn .Read (ctx )
if err != nil {
return relayproto .RelayToClientMsg {}, err
}
return relayproto .ParseRelayToClientMsgNoCopy (data , c .version )
}
func (c *Client ) Close () error {
return c .conn .Close (websocket .StatusNormalClosure , "" )
}
func (c *Client ) handshake (ctx context .Context , sk key .SecretKey ) error {
ctx , cancel := context .WithTimeout (ctx , 30 *time .Second )
defer cancel ()
frame , err := c .readFrame (ctx )
if err != nil {
return fmt .Errorf ("%w: reading challenge: %v" , ErrHandshake , err )
}
switch f := frame .(type ) {
case *relayproto .ServerChallenge :
auth := relayproto .NewClientAuth (sk , *f )
if err := c .writeFrame (ctx , auth .AppendTo (nil )); err != nil {
return fmt .Errorf ("%w: sending client auth: %v" , ErrHandshake , err )
}
return c .expectConfirmation (ctx )
case *relayproto .ServerConfirmsAuth :
return nil
case *relayproto .ServerDeniesAuth :
return fmt .Errorf ("%w: %s" , relayproto .ErrServerDeniedAuth , f .Reason )
default :
return fmt .Errorf ("%w: unexpected first frame %T" , ErrHandshake , frame )
}
}
func (c *Client ) expectConfirmation (ctx context .Context ) error {
frame , err := c .readFrame (ctx )
if err != nil {
return fmt .Errorf ("%w: reading confirmation: %v" , ErrHandshake , err )
}
switch f := frame .(type ) {
case *relayproto .ServerConfirmsAuth :
return nil
case *relayproto .ServerDeniesAuth :
return fmt .Errorf ("%w: %s" , relayproto .ErrServerDeniedAuth , f .Reason )
default :
return fmt .Errorf ("%w: unexpected frame %T" , ErrHandshake , frame )
}
}
func (c *Client ) readFrame (ctx context .Context ) (any , error ) {
_ , data , err := c .conn .Read (ctx )
if err != nil {
return nil , err
}
return relayproto .ParseHandshakeFrame (data )
}
func (c *Client ) writeFrame (ctx context .Context , data []byte ) error {
return c .conn .Write (ctx , websocket .MessageBinary , data )
}
func websocketURL(u netaddr .RelayURL ) (string , error ) {
parsed := u .URL ()
if parsed == nil {
return "" , errors .New ("relayclient: empty relay url" )
}
dial := *parsed
dial .Path = relayPath
switch strings .ToLower (dial .Scheme ) {
case "http" , "ws" :
dial .Scheme = "ws"
default :
dial .Scheme = "wss"
}
return dial .String (), nil
}
var _ = url .URL {}
The pages are generated with Golds v0.8.4 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .