diff --git a/skelly.go b/skelly.go index 7b2c91f..fa8b031 100644 --- a/skelly.go +++ b/skelly.go @@ -7,170 +7,290 @@ import ( "fmt" "log" "net" - "strconv" "strings" "time" ) +// IRC color & control codes +const ( + bold = "\x02" + italic = "\x1D" + underline = "\x1F" + reverse = "\x16" + reset = "\x0f" + white = "00" + black = "01" + blue = "02" + green = "03" + red = "04" + brown = "05" + purple = "06" + orange = "07" + yellow = "08" + lightGreen = "09" + cyan = "10" + lightCyan = "11" + lightBlue = "12" + pink = "13" + grey = "14" + lightGrey = "15" +) + var ( - server string - port string - useSSL bool - channel string - key string + // Connection settings + server string + port int + channel string + key string + password string + ipv4 bool + ipv6 bool + vhost string + + // SSL settings + useSSL bool + sslVerify bool + sslCert string + sslPass string + + // Bot settings + nick string + user string + real string + nickserv string + operserv string + mode string + flood int ) func init() { - flag.StringVar(&server, "server", "", "The IRC server address (e.g., 1.2.3.4)") - flag.StringVar(&port, "port", "6667", "The port of the IRC server (e.g., 6667)") - flag.BoolVar(&useSSL, "ssl", false, "Whether to use SSL or not") - flag.StringVar(&channel, "channel", "", "The IRC channel to join (e.g., #channelName)") - flag.StringVar(&key, "key", "", "The IRC channel key") + flag.StringVar(&server, "server", "", "The IRC server address.") + flag.IntVar(&port, "port", 6667, "The port number for the IRC server.") + flag.StringVar(&channel, "channel", "", "The IRC channel to join.") + flag.StringVar(&key, "key", "", "The key (password) for the IRC channel, if required.") + flag.StringVar(&password, "password", "", "The password for the IRC server.") + flag.BoolVar(&ipv4, "v4", false, "Use IPv4 for the connection.") + flag.BoolVar(&ipv6, "v6", false, "Use IPv6 for the connection.") + flag.StringVar(&vhost, "vhost", "", "The VHOST to use for connection.") + flag.BoolVar(&useSSL, "ssl", false, "Use SSL for the connection.") + flag.BoolVar(&sslVerify, "ssl-verify", false, "Verify SSL certificates.") + flag.StringVar(&sslCert, "ssl-cert", "", "The SSL certificate to use for the connection.") + flag.StringVar(&sslPass, "ssl-pass", "", "The SSL certificate password.") + flag.StringVar(&nick, "nick", "skelly", "The nickname to use for the bot.") + flag.StringVar(&user, "user", "skelly", "The username to use for the bot.") + flag.StringVar(&real, "real", "Development Bot", "The realname to use for the bot.") + flag.StringVar(&mode, "mode", "+B", "The mode to set on the bot's nickname.") + flag.StringVar(&nickserv, "nickserv", "", "The password for the bot's nickname to be identified with NickServ.") + flag.StringVar(&operserv, "operserv", "", "The password for the bot's nickname to be identified with OperServ.") + flag.IntVar(&flood, "flood", 3, "Delay between command usage.") flag.Parse() } -const ( - nickname = "[dev]skelly" - username = "golang" - realname = "IRC Bot Skeleton in Golang" -) - -func checkArgs() { - if server == "" || channel == "" { - log.Fatal("Both server and channel arguments are required.") - } else if channel[0] != '#' { - channel = "#" + channel // Using # on the commandline requires escaping, lets just make it optional +func logfmt(option string, message string) string { + switch option { + case "DEBUG": + return fmt.Sprintf("\033[95m%s\033[0m [\033[95mDEBUG\033[0m] %s", getnow(), message) + case "ERROR": + return fmt.Sprintf("\033[95m%s\033[0m [\033[31mERROR\033[0m] %s", getnow(), message) + case "SEND": + return fmt.Sprintf("\033[95m%s\033[0m [\033[92mSEND\033[0m] %s", getnow(), message) + case "RECV": + return fmt.Sprintf("\033[95m%s\033[0m [\033[96mRECV\033[0m] %s", getnow(), message) } - num, err := strconv.Atoi(port) - if err != nil { - log.Fatal("Invalid port number.") - } else if num < 1 || num > 65535 { - log.Fatal("Port number must be between 1 and 65535.") + return fmt.Sprintf("\033[95m%s\033[0m [\033[95mDEBUG\033[0m] %s", getnow(), message) // This should never happen +} + +func color(msg string, foreground string, background string) string { + if background != "" { + return fmt.Sprintf("\x03%s,%s%s%s", foreground, background, msg, reset) + } + return fmt.Sprintf("\x03%s%s%s", foreground, msg, reset) +} + +type Bot struct { + nickname string + username string + realname string + conn net.Conn + reader *bufio.Reader + writer *bufio.Writer + last time.Time + slow bool +} + +func Skeleton() *Bot { + return &Bot{ + nickname: "skeleton", + username: "skelly", + realname: "Development Bot", } } -func main() { - checkArgs() +func (bot *Bot) Connect() error { + address := fmt.Sprintf("%s:%d", server, port) - fullServer := fmt.Sprintf("%s:%s", server, port) - - var conn net.Conn - var err error - - if useSSL { - conn, err = tls.Dial("tcp", fullServer, &tls.Config{InsecureSkipVerify: true}) - } else { - conn, err = net.Dial("tcp", fullServer) + var networkType string + switch { + case ipv4: + networkType = "tcp4" + case ipv6: + networkType = "tcp6" + default: + networkType = "tcp" } + + var dialer net.Dialer + + if vhost != "" { + localAddr, err := net.ResolveTCPAddr(networkType, vhost+":0") + if err != nil { + return fmt.Errorf("failed to resolve local address: %w", err) + } + dialer.LocalAddr = localAddr + } + + var err error + if useSSL { + tlsConfig := &tls.Config{ + InsecureSkipVerify: !sslVerify, + } + + if sslCert != "" { + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(sslCert, sslPass) + if err != nil { + return fmt.Errorf("failed to load SSL certificate: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + bot.conn, err = tls.DialWithDialer(&dialer, networkType, address, tlsConfig) + } else { + bot.conn, err = dialer.Dial(networkType, address) + } + if err != nil { - log.Printf("Failed to connect: %v", err) + return fmt.Errorf("failed to dial: %w", err) + } + + bot.reader = bufio.NewReader(bot.conn) + bot.writer = bufio.NewWriter(bot.conn) + + if password != "" { + bot.raw("PASS " + password) + } + bot.raw(fmt.Sprintf("USER %s 0 * :%s", user, real)) + bot.raw("NICK " + nick) + + return nil +} + +func (bot *Bot) raw(data string) { + if bot.writer != nil { + bot.writer.WriteString(data + "\r\n") + bot.writer.Flush() + if strings.Split(data, " ")[0] == "PONG" { + fmt.Println(logfmt("SEND", "\033[93m"+data+"\033[0m")) + } else { + fmt.Println(logfmt("SEND", data)) + } + } +} + +func (bot *Bot) sendMsg(target string, msg string) { + bot.raw(fmt.Sprintf("PRIVMSG %s :%s", target, msg)) +} + +func (bot *Bot) handle(data string) { + parts := strings.Fields(data) + + if len(parts) < 2 { + return + } + + if parts[0] != "PING" { + parts[1] = "\033[38;5;141m" + parts[1] + "\033[0m" + } + coloredData := strings.Join(parts, " ") + fmt.Println(logfmt("RECV", coloredData)) + + parts = strings.Fields(data) + if parts[0] == "PING" { + bot.raw("PONG " + parts[1]) + return + } else { + command := parts[1] + switch command { + case "001": // RPL_WELCOME + bot.raw("MODE " + nick + " " + mode) + + if nickserv != "" { + bot.raw("PRIVMSG NickServ :IDENTIFY " + nickserv) + } + + if operserv != "" { + bot.raw("OPER " + nick + " " + operserv) + } + + go func() { + time.Sleep(15 * time.Second) + if key != "" { + bot.raw("JOIN " + channel + " " + key) + } else { + bot.raw("JOIN " + channel) + } + }() + case "PRIVMSG": + bot.eventPrivMsg(data) + } + } +} + +func getnow() string { + return time.Now().Format("03:04:05") +} + +func (bot *Bot) eventPrivMsg(data string) { + parts := strings.Split(data, " ") + ident := strings.TrimPrefix(parts[0], ":") + nick := strings.Split(ident, "!")[0] + target := parts[2] + msg := strings.Join(parts[3:], " ")[1:] + + if target == bot.nickname { + // Private message handling + } else if strings.HasPrefix(target, "#") { + if target == channel { + if msg == "!test" { + bot.sendMsg(channel, nick+": Test successful!") + } + } + } +} +func main() { + for { + fmt.Printf("\033[90m%s\033[0m [\033[95mDEBUG\033[0m] Connecting to %s:%d and joining %s\n", getnow(), server, port, channel) + + bot := Skeleton() + err := bot.Connect() + + if err != nil { + log.Printf("\033[90m%s\033[0m [\033[31mERROR\033[0m]\033[93m Failed to connect to server! Retrying in 15 seconds... \033[90m(%v)\033[0m", getnow(), err) + } else { + for { + line, _, err := bot.reader.ReadLine() + if err != nil { + log.Printf("\033[90m%s\033[0m [\033[31mERROR\033[0m]\033[93m Lost connection to server! Retrying in 15 seconds... \033[90m(%v)\033[0m", getnow(), err) + break + } + bot.handle(string(line)) + } + } + + if bot.conn != nil { + bot.conn.Close() + } + time.Sleep(15 * time.Second) } - - messageChannel := make(chan string, 100) // for received IRC messages - sendChannel := make(chan string, 100) // for messages to send to the IRC server - quit := make(chan struct{}) - - timeoutDuration := 300 * time.Second - timeoutTimer := time.NewTimer(timeoutDuration) - - go func() { - reader := bufio.NewReader(conn) - for { - line, err := reader.ReadString('\n') - if err != nil { - log.Println("Error reading from server:", err) - conn.Close() - close(quit) - return - } - select { - case messageChannel <- line: - timeoutTimer.Reset(timeoutDuration) - case <-quit: - return - case <-timeoutTimer.C: - log.Println("No data received for 300 seconds. Reconnecting...") - conn.Close() - close(quit) - return - } - } - }() - - go func() { - for { - select { - case message := <-sendChannel: - data := []byte(message) - if len(data) > 510 { - data = data[:510] - } - _, err := conn.Write(append(data, '\r', '\n')) - if err != nil { - log.Println("Error writing to server:", err) - conn.Close() - close(quit) - return - } - case <-quit: - return - } - } - }() - - // Initial handshake - sendChannel <- fmt.Sprintf("NICK %s", nickname) - sendChannel <- fmt.Sprintf("USER %s 0 * :%s", username, realname) - - for { - dataHandler(<-messageChannel, sendChannel) - } -} - -func eventPrivate(nick, ident, msg string) { - fmt.Println("Private message from", nick, ":", msg) -} - -func eventMessage(nick, ident, channel, msg string) { - fmt.Println("Channel message from", nick, ":", msg) - if ident == "acidvegas!~stillfree@most.dangerous.motherfuck" { - args := strings.Split(msg, " ") - switch args[0] { - case "!masscan": - fmt.Println("The value is a") - case "!nmap": - fmt.Println("The value is b") - case "!httpx": - fmt.Println("The value is c") - case "!nuclei": - fmt.Println("The value is c") - default: - fmt.Println("Unknown value") - } - } -} - -func dataHandler(data string, sendChannel chan<- string) { - fmt.Println(data) - parts := strings.Split(data, " ") - - if parts[0] == "PING" { - sendChannel <- fmt.Sprintf("PONG %s", parts[1]) - } else if parts[1] == "001" { // RPL_WELCOME - time.Sleep(5 * time.Second) // JOIN channel delay after connection, required for many networks with flood protection / mitigation - sendChannel <- fmt.Sprintf("JOIN %s %s", channel, key) - } else if parts[1] == "KICK" { - return - } else if len(parts) > 3 && parts[1] == "PRIVMSG" { - nick := strings.Split(parts[0], "!")[0][1:] - ident := strings.Split(parts[0], ":")[1] - target := parts[2] - msg := strings.Join(parts[3:], " ")[1:] - if target == nickname { // Private Messages - eventPrivate(nick, ident, msg) - } else if target == channel { // Channel Messages - eventMessage(nick, ident, channel, msg) - } - } }