skeleton

- bot skeleton for irc
git clone git://git.acid.vegas/skeleton.git
Log | Files | Refs | Archive | README | LICENSE

commit bafe01a0917b783fe94a9adc27dfaaf3bf8bb4b0
parent 0df93b8f80c82295df540d2a1386ee481f9988e3
Author: acidvegas <acid.vegas@acid.vegas>
Date: Tue, 12 Dec 2023 10:10:45 -0500

Updated golang ekeletn for review

Diffstat:
Mskelly.go | 366++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------

1 file changed, 243 insertions(+), 123 deletions(-)

diff --git 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 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)
+	}
+	return fmt.Sprintf("\033[95m%s\033[0m [\033[95mDEBUG\033[0m] %s", getnow(), message) // This should never happen
+}
 
-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 color(msg string, foreground string, background string) string {
+	if background != "" {
+		return fmt.Sprintf("\x03%s,%s%s%s", foreground, background, msg, reset)
 	}
-	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("\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 networkType string
+	switch {
+	case ipv4:
+		networkType = "tcp4"
+	case ipv6:
+		networkType = "tcp6"
+	default:
+		networkType = "tcp"
+	}
 
-	var conn net.Conn
-	var err error
+	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 {
-		conn, err = tls.Dial("tcp", fullServer, &tls.Config{InsecureSkipVerify: true})
+		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 {
-		conn, err = net.Dial("tcp", fullServer)
+		bot.conn, err = dialer.Dial(networkType, address)
 	}
+
 	if err != nil {
-		log.Printf("Failed to connect: %v", err)
-		time.Sleep(15 * time.Second)
+		return fmt.Errorf("failed to dial: %w", err)
 	}
 
-	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{})
+	bot.reader = bufio.NewReader(bot.conn)
+	bot.writer = bufio.NewWriter(bot.conn)
 
-	timeoutDuration := 300 * time.Second
-	timeoutTimer := time.NewTimer(timeoutDuration)
+	if password != "" {
+		bot.raw("PASS " + password)
+	}
+	bot.raw(fmt.Sprintf("USER %s 0 * :%s", user, real))
+	bot.raw("NICK " + nick)
 
-	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
-			}
+	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))
 		}
-	}()
-
-	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
+	}
+}
+
+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)
 			}
-		}
-	}()
 
-	// Initial handshake
-	sendChannel <- fmt.Sprintf("NICK %s", nickname)
-	sendChannel <- fmt.Sprintf("USER %s 0 * :%s", username, realname)
+			if operserv != "" {
+				bot.raw("OPER " + nick + " " + operserv)
+			}
 
-	for {
-		dataHandler(<-messageChannel, sendChannel)
+			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 eventPrivate(nick, ident, msg string) {
-	fmt.Println("Private message from", nick, ":", msg)
+func getnow() string {
+	return time.Now().Format("03:04:05")
 }
 
-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 (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)
 
-func dataHandler(data string, sendChannel chan<- string) {
-	fmt.Println(data)
-	parts := strings.Split(data, " ")
+		bot := Skeleton()
+		err := bot.Connect()
 
-	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)
+		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)
 	}
 }