diff --git a/examples/simple-tor.go/simple-tor.go b/examples/simple-tor.go/simple-tor.go new file mode 100644 index 0000000..baeeecc --- /dev/null +++ b/examples/simple-tor.go/simple-tor.go @@ -0,0 +1,52 @@ +package main + +import ( + "github.com/thoj/go-ircevent" + "crypto/tls" + "log" + "os" +) + +const addr = "libera75jm6of4wxpxt4aynol3xjmbtxgfyjpu34ss4d7r7q2v5zrpyd.onion:6697" + +// This demos connecting to Libera.Chat over TOR using SASL EXTERNAL and a TLS +// client cert. It assumes a TOR SOCKS service is running on localhost:9050 +// and requires an existing account with a fingerprint already registered. See +// https://libera.chat/guides/connect#accessing-liberachat-via-tor for details. +// +// Pass the full path to your cert and key on the command line like so: +// $ go run simple-tor.go my-nick my-cert.pem my-key.pem + +func main() { + os.Setenv("ALL_PROXY", "socks5h://localhost:9050") + nick, certFile := os.Args[1], os.Args[2] + keyFile := certFile + if len(os.Args) == 4 { + keyFile = os.Args[3] + } + clientCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + log.Fatal(err) + } + ircnick1 := nick + irccon := irc.IRC(ircnick1, nick) + irccon.VerboseCallbackHandler = true + irccon.UseSASL = true + irccon.SASLMech = "EXTERNAL" + irccon.Debug = true + irccon.UseTLS = true + irccon.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{clientCert}, + } + irccon.AddCallback("001", func(e *irc.Event) {}) + irccon.AddCallback("376", func(e *irc.Event) { + log.Println("Quitting") + irccon.Quit() + }) + err = irccon.Connect(addr) + if err != nil { + log.Fatal(err) + } + irccon.Loop() +} diff --git a/go.mod b/go.mod index b7c604b..e1f2f41 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/thoj/go-ircevent go 1.12 -require golang.org/x/text v0.3.2 +require ( + golang.org/x/net v0.0.0-20210614182718-04defd469f4e + golang.org/x/text v0.3.6 +) diff --git a/go.sum b/go.sum index 63c9200..2dc5506 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,10 @@ +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/irc.go b/irc.go index 03eb318..a02b2b7 100644 --- a/irc.go +++ b/irc.go @@ -31,6 +31,7 @@ import ( "strings" "time" + "golang.org/x/net/proxy" "golang.org/x/text/encoding" ) @@ -461,15 +462,14 @@ func (irc *Connection) Connect(server string) error { return errors.New("empty 'user'") } - if irc.UseTLS { - dialer := &net.Dialer{Timeout: irc.Timeout} - irc.socket, err = tls.DialWithDialer(dialer, "tcp", irc.Server, irc.TLSConfig) - } else { - irc.socket, err = net.DialTimeout("tcp", irc.Server, irc.Timeout) - } + dialer := proxy.FromEnvironmentUsing(&net.Dialer{Timeout: irc.Timeout}) + irc.socket, err = dialer.Dial("tcp", irc.Server) if err != nil { return err } + if irc.UseTLS { + irc.socket = tls.Client(irc.socket, irc.TLSConfig) + } if irc.Encoding == nil { irc.Encoding = encoding.Nop diff --git a/irc_sasl.go b/irc_sasl.go index be02380..944496b 100644 --- a/irc_sasl.go +++ b/irc_sasl.go @@ -31,8 +31,8 @@ func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) (callbacks } } if e.Arguments[1] == "ACK" && listContains(e.Arguments[2], "sasl") { - if irc.SASLMech != "PLAIN" { - result <- &SASLResult{true, errors.New("only PLAIN is supported")} + if irc.SASLMech != "PLAIN" && irc.SASLMech != "EXTERNAL" { + result <- &SASLResult{true, errors.New("only PLAIN and EXTERNAL supported")} } irc.SendRaw("AUTHENTICATE " + irc.SASLMech) } @@ -41,6 +41,10 @@ func (irc *Connection) setupSASLCallbacks(result chan<- *SASLResult) (callbacks callbacks = append(callbacks, CallbackID{"CAP", id}) id = irc.AddCallback("AUTHENTICATE", func(e *Event) { + if irc.SASLMech == "EXTERNAL" { + irc.SendRaw("AUTHENTICATE +") + return + } str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword))) irc.SendRaw("AUTHENTICATE " + str) }) diff --git a/irc_sasl_test.go b/irc_sasl_test.go index 1b363d4..4020812 100644 --- a/irc_sasl_test.go +++ b/irc_sasl_test.go @@ -41,3 +41,49 @@ func TestConnectionSASL(t *testing.T) { } irccon.Loop() } + + +// 1. Register fingerprint with IRC network +// 2. Add SASLKeyPem="-----BEGIN PRIVATE KEY-----..." +// and SASLCertPem="-----BEGIN CERTIFICATE-----..." +// to CI environment as masked variables +func TestConnectionSASLExternal(t *testing.T) { + SASLServer := "irc.freenode.net:7000" + keyPem := os.Getenv("SASLKeyPem") + certPem := os.Getenv("SASLCertPem") + + if certPem == "" || keyPem == "" { + t.Skip("Env vars SASLKeyPem SASLCertPem not present, skipping") + } + if testing.Short() { + t.Skip("skipping test in short mode.") + } + cert, err := tls.X509KeyPair([]byte(certPem), []byte(keyPem)) + if err != nil { + t.Fatalf("SASL EXTERNAL cert creation failed: %s", err) + } + + irccon := IRC("go-eventirc", "go-eventirc") + irccon.VerboseCallbackHandler = true + irccon.Debug = true + irccon.UseTLS = true + irccon.UseSASL = true + irccon.SASLMech = "EXTERNAL" + irccon.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{cert}, + } + irccon.AddCallback("001", func(e *Event) { irccon.Join("#go-eventirc") }) + + irccon.AddCallback("366", func(e *Event) { + irccon.Privmsg("#go-eventirc", "Test Message SASL EXTERNAL\n") + time.Sleep(2 * time.Second) + irccon.Quit() + }) + + err = irccon.Connect(SASLServer) + if err != nil { + t.Fatalf("SASL EXTERNAL failed: %s", err) + } + irccon.Loop() +}