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)
}
}
|