package ircclient import ( "bufio" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "fmt" "log" "math/big" rnd "math/rand" "net" "strings" "testing" "time" ) // Reuse the KeyPair tls.Certificate, it is valid for 1h var KeyPair tls.Certificate var HasKeyPair bool // Display debug output when running tests. const TEST_DEBUG_OUTPUT bool = false // Give Key Pair for server TLS connection // // This reuses the KeyPair if already generated in this run. // Otherwise, it call generateKeyPair to do the actual certificate generation. func GenerateKeyPair() (keypair tls.Certificate) { if !HasKeyPair { KeyPair = generateKeyPair() HasKeyPair = true } return KeyPair } // Generate certificate for server TLS connection func generateKeyPair() (keypair tls.Certificate) { // generate test certificate priv, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) durationBefore, _ := time.ParseDuration("-1h") notBefore := time.Now().Add(durationBefore) durationAfter, _ := time.ParseDuration("1h") notAfter := time.Now().Add(durationAfter) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 64) serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"Test Certificate"}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, } template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1")) template.IPAddresses = append(template.IPAddresses, net.ParseIP("::")) derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) b, _ := x509.MarshalECPrivateKey(priv) k := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) listenerKeyPair, _ := tls.X509KeyPair(c, k) return listenerKeyPair } // Setup TLS Socket for IRCD connection testing. func setupTLSSocket() (listen net.Listener, addr string) { // establish network socket connection to set Comm_handle var err error var listener net.Listener var address string var tlsconfig tls.Config var keypair tls.Certificate = GenerateKeyPair() tlsconfig.Certificates = make([]tls.Certificate, 0) tlsconfig.Certificates = append(tlsconfig.Certificates, keypair) listener, err = tls.Listen("tcp", "127.0.0.1:0", &tlsconfig) if err != nil { panic(err) } // I only need address for making the connection. // Get address of listening socket address = listener.Addr().String() return listener, address } // Setup Socket for IRCD connection testing. func setupSocket() (listen net.Listener, addr string) { // establish network socket connection to set Comm_handle var err error var listener net.Listener var address string listener, err = net.Listen("tcp", "127.0.0.1:0") if err != nil { panic(err) } // I only need address for making the connection. // Get address of listening socket address = listener.Addr().String() return listener, address } // Write to IRCD server (testing) func ircWrite(server net.Conn, output string, t *testing.T) { if TEST_DEBUG_OUTPUT { t.Logf(">> %s\n", output) } _, err := server.Write([]byte(output + "\r\n")) if err != nil { t.Error("server.Write:", err) } } var abortAfter int = 150 // Milliseconds to abort part 3 /* Mock up a test IRCD server part 1 : nick, user part 2 : identify to services (if not SASL/SASL failed) part 3 : quit after abortAfter ms Features: if nick == "bad", return 443 Nick already in use. JOIN #kick, kicks once from the channel. SASL authentication testing. TLS support, we make our own certificates. See generateKeyPair() Any messages sent to "echo" are reflected back to the client. Any messages to missing/#missing are returned 404/401. After abortAfter milliseconds, we close the connection. */ func ircServer(listener net.Listener, t *testing.T, config *IRCClient) { var server net.Conn var err error server, err = listener.Accept() if err != nil { t.Error("Failed to accept connection.") return } listener.Close() var reader *bufio.Reader = bufio.NewReader(server) var output, line, expect string var ping int64 = rnd.Int63() output = fmt.Sprintf("PING :%X", ping) ircWrite(server, output, t) var parts []string var hasNick, hasUser, hasPing, hasPass bool var capSASL, successSASL bool var part1 bool // part 1 : User, Nick, ServerPass and Ping reply for !part1 { line, err = reader.ReadString('\n') if err == nil { line = strings.Trim(line, "\r\n") // process the received line here parts = strings.Split(line, " ") if TEST_DEBUG_OUTPUT { t.Logf("<< %s", line) } switch parts[0] { case "CAP": if config.UseTLS && config.UseSASL { if line == "CAP REQ :sasl" { // Acknowledge we support SASL ircWrite(server, ":irc.red-green.com CAP * ACK :sasl", t) capSASL = true } if line == "CAP END" { capSASL = true if successSASL { part1 = true } } } case "AUTHENTICATE": if capSASL { if line == "AUTHENTICATE PLAIN" { ircWrite(server, "AUTHENTICATE +", t) } else { // Process SASL auth message var auth64 string = parts[1] byteauth, _ := base64.StdEncoding.DecodeString(auth64) var auth string = string(byteauth) auth = strings.ReplaceAll(auth, "\x00", " ") t.Log(auth) expect = fmt.Sprintf(" %s %s", config.Nick, config.Password) if expect != auth { t.Errorf("Got %s, Expected %s", auth, expect) ircWrite(server, fmt.Sprintf(":irc.red-green.com 904 %s :SASL authentication failed", config.Nick), t) } else { // Success! ircWrite(server, fmt.Sprintf(":irc.red-green.com 900 %s %s!%s@127.0.0.1 %s :You are now logged in as %s.", config.Nick, config.Nick, config.Username, config.Nick, config.Nick), t) ircWrite(server, fmt.Sprintf(":irc.red-green.com 903 %s :SASL authentication successful", config.Nick), t) successSASL = true } } } case "PASS": expect = fmt.Sprintf("PASS %s", config.ServerPassword) if expect != line { t.Errorf("Got %s, Expected %s", line, expect) } else { hasPass = true } case "NICK": expect = fmt.Sprintf("NICK %s", config.MyNick) if expect != line { t.Errorf("Got %s, Expected %s", line, expect) } else { if config.MyNick == "bad" { // throw bad nick here // ircWrite(server, fmt.Sprintf(":irc.red-green.com 433 :Nick already in use."), t) ircWrite(server, ":irc.red-green.com 433 :Nick already in use.", t) } hasNick = true } case "USER": // USER meow-bot 0 * :Meooow! bugz is my owner. expect = fmt.Sprintf("USER %s 0 * :%s", config.Username, config.Realname) if expect != line { t.Errorf("Got %s, Expected %s", line, expect) } else { hasUser = true } case "PONG": expect = fmt.Sprintf("PONG %X", ping) if expect != line { t.Errorf("Got %s, Expected %s", line, expect) } else { hasPing = true } } if !part1 { if !capSASL && hasNick && hasUser && hasPing && ((config.ServerPassword == "") || hasPass) { part1 = true } } } else { t.Error("Read Error:", err) server.Close() return } } if !part1 { t.Error("Expected to pass part1 (user/nick/pong)") } // Display MOTD for _, line = range []string{ ":irc.red-green.com 001 %s :Welcome to the RedGreen IRC Network", ":irc.red-green.com 002 %s :Your host is irc.red-green.com, running version UnrealIRCd-5.2.0.1", ":irc.red-green.com 005 %s AWAYLEN=307 BOT=B CASEMAPPING=ascii CHANLIMIT=#:10 CHANMODES=beI,kLf,lH,psmntirzMQNRTOVKDdGPZSCc CHANNELLEN=32 CHANTYPES=# CLIENTTAGDENY=*,-draft/typing,-typing DEAF=d ELIST=MNUCT EXCEPTS EXTBAN=~,GptmTSOcarnqjf :are supported by this server", ":irc.red-green.com 005 %s HCN INVEX KICKLEN=307 KNOCK MAP MAXCHANNELS=10 MAXLIST=b:60,e:60,I:60 MAXNICKLEN=30 MINNICKLEN=0 MODES=12 NAMESX NETWORK=RedGreen :are supported by this server", ":irc.red-green.com 005 %s NICKLEN=30 PREFIX=(qaohv)~&@%%+ QUITLEN=307 SAFELIST SILENCE=15 STATUSMSG=~&@%%+ TARGMAX=DCCALLOW:,ISON:,JOIN:,KICK:4,KILL:,LIST:,NAMES:1,NOTICE:1,PART:,PRIVMSG:4,SAJOIN:,SAPART:,TAGMSG:1,USERHOST:,USERIP:,WATCH:,WHOIS:1,WHOWAS:1 TOPICLEN=360 UHNAMES USERIP WALLCHOPS WATCH=128 :are supported by this server", ":irc.red-green.com 005 %s WATCHOPTS=A WHOX :are supported by this server", ":irc.red-green.com 375 %s :- irc.red-green.com Message of the Day -", ":irc.red-green.com 372 %s :- ", ":irc.red-green.com 376 %s :End of /MOTD command.", } { output = fmt.Sprintf(line, config.Nick) ircWrite(server, output, t) } if config.UseSASL { if !successSASL { log.Println("Failed SASL Authentication.") } } // part 2: nickserv/register (if not already registered with SASL) var part2 bool if successSASL { ircWrite(server, fmt.Sprintf(":NickServ MODE %s :+r", config.Nick), t) part2 = true } else { if config.Password != "" { for _, line = range []string{":NickServ!services@services.red-green.com NOTICE %s :This nickname is registered and protected. If it is your", ":NickServ!services@services.red-green.com NOTICE %s :nick, type \x02/msg NickServ IDENTIFY \x1fpassword\x1f\x02. Otherwise,"} { output = fmt.Sprintf(line, config.Nick) ircWrite(server, output, t) } } else { // No password, so we can't register. Skip this part. part2 = true } } for !part2 { line, err = reader.ReadString('\n') if err == nil { line = strings.Trim(line, "\r\n") // process the received line here parts = strings.Split(line, " ") t.Logf("<< %s", line) switch parts[0] { case "NS": expect = fmt.Sprintf("NS IDENTIFY %s", config.Password) if expect != line { t.Errorf("Got %s, Expected %s", line, expect) } // ok, mark the user as registered output = fmt.Sprintf(":NickServ!services@services.red-green.com NOTICE %s :Password accepted - you are now recognized.", config.Nick) ircWrite(server, output, t) output = fmt.Sprintf(":NickServ MODE %s :+r", config.Nick) ircWrite(server, output, t) part2 = true } } else { t.Error("Read Error:", err) server.Close() return } } if !part2 { t.Error("Expected to pass part2 (ns identify/+r)") } time.AfterFunc(time.Millisecond*time.Duration(abortAfter), func() { server.Close() }) t.Log("Ok, Identified...") var Kicked bool for { line, err = reader.ReadString('\n') if err == nil { line = strings.Trim(line, "\r\n") // process the received line here parts = strings.Split(line, " ") if TEST_DEBUG_OUTPUT { t.Logf("<< %s", line) } switch parts[0] { case "JOIN": for _, channel := range strings.Split(parts[1], ",") { output = fmt.Sprintf(":%s JOIN :%s", config.MyNick, channel) ircWrite(server, output, t) output = fmt.Sprintf(":irc.server 332 %s %s :Topic for (%s)", config.MyNick, channel, channel) ircWrite(server, output, t) output = fmt.Sprintf(":irc.server 333 %s %s user %d", config.MyNick, channel, time.Now().Unix()) ircWrite(server, output, t) if strings.Contains(channel, "kick") { if !Kicked { Kicked = true output = fmt.Sprintf("user KICK %s %s :Get out", channel, config.MyNick) ircWrite(server, output, t) } } } } switch parts[0] { case "PRIVMSG", "NOTICE": if parts[1] == "echo" { parts[2] = parts[2][1:] // echo user, return whatever was sent back to them. output = fmt.Sprintf(":%s %s %s :%s", "echo", parts[0], config.MyNick, strings.Join(parts[2:], " ")) ircWrite(server, output, t) } if strings.Contains(parts[1], "missing") { // Sending to missing user or channel. var number int if strings.Contains(parts[1], "#") { number = 404 } else { number = 401 } output = fmt.Sprintf(":irc.red-green.com %d %s %s :No such nick/channel", number, config.GetNick(), parts[1]) ircWrite(server, output, t) } case "NICK": output = fmt.Sprintf(":%s NICK :%s", config.MyNick, parts[1]) ircWrite(server, output, t) case "QUIT": output = fmt.Sprintf("ERROR: Closing link:%s", config.MyNick) ircWrite(server, output, t) server.Close() } } else { t.Log("Read Error:", err) return } } }