123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- 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!%[email protected] %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{":[email protected] NOTICE %s :This nickname is registered and protected. If it is your",
- ":[email protected] 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(":[email protected] 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
- }
- }
- }
|