irc-client.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. // An IRC-Client library.
  2. package ircclient
  3. import (
  4. "bufio"
  5. "crypto/tls"
  6. "crypto/x509"
  7. "encoding/base64"
  8. "fmt"
  9. "io"
  10. "log"
  11. "math/rand"
  12. "net"
  13. "os"
  14. "os/signal"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "syscall"
  19. "time"
  20. )
  21. // get client version information from runtime package.
  22. var VERSION string = GetModuleVersion()
  23. func GetModuleVersion() string {
  24. modules := GetModules()
  25. version, has := modules["git.red-green.com/RedGreen/irc-client"]
  26. if has {
  27. return version
  28. }
  29. return "git.red-green.com/irc-client"
  30. }
  31. // Is string in slice?
  32. func StrInArray(strings []string, str string) bool {
  33. for _, s := range strings {
  34. if s == str {
  35. return true
  36. }
  37. }
  38. return false
  39. }
  40. // IRC Message
  41. type IRCMsg struct {
  42. MsgParts []string // Message parts
  43. From string // Message From
  44. To string // Message To
  45. Cmd string // Command
  46. Msg string // Message
  47. }
  48. // Convert nick to lowercase following IRC capitalization rules.
  49. func NameLower(name string) string {
  50. // uppercase: []\-
  51. // lowercase: {}|^
  52. var result string = strings.ToLower(name)
  53. result = strings.ReplaceAll(result, "[", "{")
  54. result = strings.ReplaceAll(result, "]", "}")
  55. result = strings.ReplaceAll(result, "\\", "|")
  56. result = strings.ReplaceAll(result, "-", "^")
  57. return result
  58. }
  59. // Check to see if nicks or channels match according to IRC rules.
  60. func Match(name1 string, name2 string) bool {
  61. return NameLower(name1) == NameLower(name2)
  62. }
  63. // Strip out the NICK part of the From message.
  64. // :[email protected] => NickServ
  65. // IRCNick return just the NICK from :Nick!name@host
  66. func IRCNick(from string) string {
  67. if from[0] == ':' {
  68. from = from[1:]
  69. }
  70. var pos int = strings.Index(from, "!")
  71. if pos != -1 {
  72. from = from[:pos]
  73. }
  74. return from
  75. }
  76. /*
  77. IRCParse - split line into IRCMsg
  78. Everything after " :" is the Msg.
  79. Everything before " :" is split into MsgParts[].
  80. If >= 3 MsgParts {
  81. To = MsgParts[2]
  82. }
  83. if >= 2 MsgParts {
  84. From = IrcNic(MsgParts[0])
  85. Cmd = MsgParts[1]
  86. } else {
  87. Cmd = MsgParts[0]
  88. }
  89. Example Messages:
  90. :irc.red-green.com 001 test :Welcome to IRC
  91. ^From ^Cmd ^Msg
  92. ^To
  93. ^0 ^1 ^2 MsgParts[]
  94. PING :1234567890
  95. ^Cmd ^Msg
  96. ^0 MsgParts[]
  97. */
  98. // IRCParse Parse an IRC line into the IRCMsg struct.
  99. func IRCParse(line string) IRCMsg {
  100. var pos int = strings.Index(line, " :")
  101. var results IRCMsg
  102. if pos != -1 {
  103. // Message is everything after " :"
  104. results.Msg = line[pos+2:]
  105. line = line[:pos]
  106. }
  107. results.MsgParts = strings.Split(line, " ")
  108. if len(results.MsgParts) >= 2 {
  109. results.From = IRCNick(results.MsgParts[0])
  110. results.Cmd = results.MsgParts[1]
  111. } else {
  112. results.Cmd = results.MsgParts[0]
  113. }
  114. if len(results.MsgParts) >= 3 {
  115. results.To = results.MsgParts[2]
  116. }
  117. // Translate CTCP and ACTION
  118. if results.Cmd == "PRIVMSG" {
  119. if (results.Msg[0] == '\x01') && (results.Msg[len(results.Msg)-1] == '\x01') {
  120. results.Cmd = "CTCP"
  121. results.Msg = results.Msg[1 : len(results.Msg)-1]
  122. if strings.HasPrefix(results.Msg, "ACTION ") {
  123. results.Cmd = "ACTION"
  124. results.Msg = results.Msg[7:]
  125. }
  126. }
  127. }
  128. return results
  129. }
  130. /*
  131. func oldIRCParse(line string) []string {
  132. var pos int = strings.Index(line, " :")
  133. var message string
  134. if pos != -1 {
  135. message = line[pos+2:]
  136. line = line[:pos]
  137. }
  138. var results []string
  139. results = strings.Split(line, " ")
  140. if message != "" {
  141. results = append(results, message)
  142. }
  143. return results
  144. }
  145. */
  146. // Sent to writer
  147. //
  148. // The To part allows the writer to throttle messages to a specific target.
  149. // When throttled, it delays and alternates between the targets.
  150. type IRCWrite struct {
  151. To string // Write To
  152. Output string // Output
  153. }
  154. // Configuration
  155. type IRCConfig struct {
  156. Port int `json:"Port"` // IRC Connection port
  157. Hostname string `json:"Hostname"` // Hostname for connection
  158. UseTLS bool `json:"UseTLS"` // Use TLS Secure connection
  159. UseSASL bool `json:"UseSASL"` // Authenticate via SASL
  160. Insecure bool `json:"Insecure"` // Allow self-signed certificates
  161. Nick string `json:"Nick"` // Client's nick
  162. Username string `json:"Username"` // Client's username
  163. Realname string `json:"Realname"` // Client's realname
  164. Password string `json:"Password"` // Password for nickserv
  165. ServerPassword string `json:"ServerPassword"` // Password for server
  166. AutoJoin []string `json:"AutoJoin"` // Channels to auto-join
  167. RejoinDelay int `json:"RejoinDelay"` // ms to rejoin
  168. Version string `json:"Version"` // Version displayed
  169. Flood_Num int `json:"FloodNum"` // Number of lines sent before considered a flood
  170. Flood_Time int `json:"FloodTime"` // Number of Seconds to track previous messages
  171. Flood_Delay int `json:"FloodDelay"` // Delay between sending when flood protection on (Milliseconds)
  172. Debug_Output bool `json:"Debug"` // Enable debug output
  173. }
  174. type IRCClient struct {
  175. IRCConfig
  176. MyNick string // Client's current nick
  177. OnExit func() // Called on exit
  178. Socket net.Conn // Connection to IRCD
  179. Reader *bufio.Reader // For reading a line at a time from the IRCD
  180. ReadChannel chan IRCMsg // Channel of parsed IRC Messages
  181. ReadEvents []string //
  182. WriteChannel chan IRCWrite // Channel for writing messages to IRCD
  183. DelChannel chan string // For deleting channel or nicks that are missing.
  184. Registered bool // Registered with services?
  185. ISupport map[string]string // IRCD capabilities (005)
  186. wg sync.WaitGroup // Safe shutdown of goroutines
  187. Mutex sync.Mutex // Guards MyNick
  188. }
  189. // Get the current nick of the client
  190. func (Config *IRCClient) GetNick() string {
  191. Config.Mutex.Lock()
  192. defer Config.Mutex.Unlock()
  193. return Config.MyNick
  194. }
  195. // Sets the current nick of the client
  196. func (Config *IRCClient) SetNick(nick string) {
  197. Config.Mutex.Lock()
  198. defer Config.Mutex.Unlock()
  199. Config.MyNick = nick
  200. }
  201. // Is channel in the AutoJoin list?
  202. func (Config *IRCClient) IsAuto(channel string) bool {
  203. return StrInArray(Config.AutoJoin, channel)
  204. }
  205. // Connect to IRCD and authenticate.
  206. //
  207. // This starts the ReaderRoutine to handle processing the lines from the IRCD.
  208. // You must setup ReadChannel if you want to process any messages.
  209. func (Config *IRCClient) Connect() bool {
  210. var err error
  211. // Set sensible defaults (if not provided),
  212. if Config.Version == "" {
  213. Config.Version = VERSION
  214. }
  215. if Config.Flood_Num == 0 {
  216. Config.Flood_Num = 5
  217. }
  218. if Config.Flood_Time == 0 {
  219. Config.Flood_Time = 10
  220. }
  221. if Config.Flood_Delay == 0 {
  222. Config.Flood_Delay = 2000
  223. }
  224. if Config.UseSASL {
  225. if !Config.UseTLS {
  226. log.Println("Can't UseSASL if not using UseTLS")
  227. Config.UseSASL = false
  228. }
  229. }
  230. Config.Registered = false
  231. if Config.ReadChannel == nil {
  232. log.Println("Warning: ReadChannel is nil. You can't received IRCMsg messages.")
  233. }
  234. if Config.UseTLS {
  235. var tlsConfig tls.Config
  236. if !Config.Insecure {
  237. // Use system default CA Certificates
  238. certPool := x509.NewCertPool()
  239. tlsConfig.ClientCAs = certPool
  240. tlsConfig.InsecureSkipVerify = false
  241. } else {
  242. tlsConfig.InsecureSkipVerify = true
  243. }
  244. Config.Socket, err = tls.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port), &tlsConfig)
  245. if err != nil {
  246. log.Fatal("tls.Dial:", err)
  247. }
  248. Config.Reader = bufio.NewReader(Config.Socket)
  249. } else {
  250. Config.Socket, err = net.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port))
  251. if err != nil {
  252. log.Fatal("net.Dial:", err)
  253. }
  254. Config.Reader = bufio.NewReader(Config.Socket)
  255. }
  256. // WriteChannel may contain a message when we're trying to PriorityWrite from sigChannel.
  257. Config.WriteChannel = make(chan IRCWrite, 3)
  258. Config.DelChannel = make(chan string)
  259. Config.ISupport = make(map[string]string)
  260. // We are connected.
  261. go Config.WriterRoutine()
  262. Config.wg.Add(1)
  263. // Registration
  264. if Config.UseTLS && Config.UseSASL {
  265. Config.PriorityWrite("CAP REQ :sasl")
  266. }
  267. if Config.ServerPassword != "" {
  268. Config.PriorityWrite(fmt.Sprintf("PASS %s", Config.ServerPassword))
  269. }
  270. // Register nick
  271. Config.MyNick = Config.Nick
  272. Config.PriorityWrite(fmt.Sprintf("NICK %s", Config.Nick))
  273. Config.PriorityWrite(fmt.Sprintf("USER %s 0 * :%s", Config.Username, Config.Realname))
  274. go Config.ReaderRoutine()
  275. Config.wg.Add(1)
  276. return true
  277. }
  278. // Write to IRCD
  279. func (Config *IRCClient) write(output string) error {
  280. var err error
  281. if Config.Debug_Output {
  282. log.Println(">>", output)
  283. }
  284. output += "\r\n"
  285. _, err = Config.Socket.Write([]byte(output))
  286. return err
  287. }
  288. // WriterRoutine a goroutine that handles flood control
  289. func (Config *IRCClient) WriterRoutine() {
  290. var err error
  291. var throttle ThrottleBuffer
  292. var Flood FloodTrack
  293. Flood.Init(Config.Flood_Num, Config.Flood_Time)
  294. defer Config.wg.Done()
  295. throttle.Init()
  296. // signal handler
  297. var sigChannel chan os.Signal = make(chan os.Signal, 1)
  298. signal.Notify(sigChannel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  299. // Change this into a select with timeout.
  300. // Timeout, if there's something to be buffered.
  301. var gotSignal bool
  302. for {
  303. if throttle.Life_sucks {
  304. select {
  305. case output := <-Config.WriteChannel:
  306. if output.To == "" {
  307. err = Config.write(output.Output)
  308. if err != nil {
  309. log.Println("Writer:", err)
  310. return
  311. }
  312. continue
  313. }
  314. throttle.push(output.To, output.Output)
  315. case <-sigChannel:
  316. if !gotSignal {
  317. gotSignal = true
  318. log.Println("SIGNAL")
  319. Config.PriorityWrite("QUIT :Received SIGINT")
  320. }
  321. continue
  322. case remove := <-Config.DelChannel:
  323. if Config.Debug_Output {
  324. log.Printf("Remove: [%s]\n", remove)
  325. }
  326. throttle.delete(remove)
  327. case <-time.After(time.Millisecond * time.Duration(Config.Flood_Delay)):
  328. var msg string = throttle.pop()
  329. err = Config.write(msg)
  330. if err != nil {
  331. log.Println("Writer:", err)
  332. }
  333. }
  334. } else {
  335. // Life is good.
  336. select {
  337. case <-sigChannel:
  338. if !gotSignal {
  339. gotSignal = true
  340. log.Println("SIGNAL")
  341. Config.PriorityWrite("QUIT :Received SIGINT")
  342. }
  343. continue
  344. case remove := <-Config.DelChannel:
  345. if Config.Debug_Output {
  346. log.Printf("Remove: [%s]\n", remove)
  347. }
  348. throttle.delete(remove)
  349. case output := <-Config.WriteChannel:
  350. if output.To == "" {
  351. err = Config.write(output.Output)
  352. if err != nil {
  353. log.Println("Writer:", err)
  354. return
  355. }
  356. continue
  357. }
  358. if Flood.Full() {
  359. throttle.push(output.To, output.Output)
  360. } else {
  361. // Flood limits not reached
  362. Flood.Save()
  363. err = Config.write(output.Output)
  364. if err != nil {
  365. log.Println("Writer:", err)
  366. }
  367. }
  368. }
  369. }
  370. }
  371. }
  372. // Shutdown the client.
  373. func (Config *IRCClient) Close() {
  374. Config.Socket.Close()
  375. Config.PriorityWrite("")
  376. if Config.OnExit != nil {
  377. Config.OnExit()
  378. }
  379. Config.wg.Wait()
  380. }
  381. // Generate random nick to change to (upon collision/433)
  382. func RandomNick(nick string) string {
  383. var result string = nick + "-"
  384. result += strconv.Itoa(rand.Intn(1000))
  385. return result
  386. }
  387. // PriorityWrite: Send output command to server immediately.
  388. //
  389. // This is never throttled.
  390. func (Config *IRCClient) PriorityWrite(output string) {
  391. Config.WriteChannel <- IRCWrite{To: "", Output: output}
  392. }
  393. // WriteTo: Send throttled output command using "to" to throttle.
  394. func (Config *IRCClient) WriteTo(to string, output string) {
  395. Config.WriteChannel <- IRCWrite{To: to, Output: output}
  396. }
  397. // Msg: Send PRIVMSG, uses WriteTo to throttle.
  398. func (Config *IRCClient) Msg(to string, message string) {
  399. Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :%s", to, message))
  400. }
  401. // Notice: Send NOTICE, uses WriteTo to throttle.
  402. func (Config *IRCClient) Notice(to string, message string) {
  403. Config.WriteTo(to, fmt.Sprintf("NOTICE %s :%s", to, message))
  404. }
  405. // Action: Send PRIVMSG CTCP ACTION, uses WriteTo to throttle.
  406. func (Config *IRCClient) Action(to string, message string) {
  407. Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :\x01ACTION %s\x01", to, message))
  408. }
  409. // Goroutine reader routine
  410. //
  411. // This reads a line at a time, delimited by '\n'.
  412. // Auto-replies to server PING.
  413. // Converts CTCP & ACTION messages to type CTCP/ACTION.
  414. // Automatically answers /VERSION and /TIME.
  415. // Handles SASL authentication.
  416. // Rejoins AutoJoin channels kicked from.
  417. func (Config *IRCClient) ReaderRoutine() {
  418. defer Config.wg.Done()
  419. var registering bool
  420. for {
  421. var line string
  422. var err error
  423. var results IRCMsg
  424. line, err = Config.Reader.ReadString('\n')
  425. if err == nil {
  426. line = strings.Trim(line, "\r\n")
  427. if Config.Debug_Output {
  428. log.Println("<<", line)
  429. }
  430. results = IRCParse(line)
  431. switch results.Cmd {
  432. case "PING":
  433. // Ping from Server
  434. Config.PriorityWrite("PONG " + results.Msg)
  435. case "005":
  436. var support string
  437. for _, support = range results.MsgParts[3:] {
  438. if strings.Contains(support, "=") {
  439. var suppart []string = strings.Split(support, "=")
  440. Config.ISupport[suppart[0]] = suppart[1]
  441. } else {
  442. Config.ISupport[support] = ""
  443. }
  444. }
  445. case "433":
  446. if !registering {
  447. // Nick already in use!
  448. var newNick string = RandomNick(Config.Nick)
  449. Config.MyNick = newNick
  450. Config.PriorityWrite("NICK " + newNick)
  451. }
  452. case "CTCP":
  453. if strings.HasPrefix(results.Msg, "PING ") {
  454. Config.WriteTo(IRCNick(results.From),
  455. fmt.Sprintf("NOTICE %s :\x01%s\x01",
  456. IRCNick(results.From),
  457. results.Msg))
  458. } else if strings.HasPrefix(results.Msg, "VERSION") {
  459. var version string
  460. if Config.Version != "" {
  461. version = Config.Version + " " + VERSION
  462. } else {
  463. version = VERSION
  464. }
  465. Config.WriteTo(IRCNick(results.From),
  466. fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01",
  467. IRCNick(results.From), version))
  468. } else if strings.HasPrefix(results.Msg, "TIME") {
  469. var now time.Time = time.Now()
  470. Config.WriteTo(IRCNick(results.From),
  471. fmt.Sprintf("NOTICE %s :\x01TIME %s\x01",
  472. IRCNick(results.From),
  473. now.Format(time.ANSIC)))
  474. }
  475. }
  476. } else {
  477. // This is likely, 2022/04/05 10:11:41 ReadString: EOF
  478. if err != io.EOF {
  479. log.Println("ReadString:", err)
  480. }
  481. close(Config.ReadChannel)
  482. return
  483. }
  484. var msg IRCMsg = results
  485. if !Config.Debug_Output {
  486. if msg.Cmd == "ERROR" || msg.Cmd[0] == '4' || msg.Cmd[0] == '5' {
  487. // Always log errors.
  488. log.Println("<<", line)
  489. }
  490. }
  491. if msg.Cmd == "401" || msg.Cmd == "404" {
  492. // No such nick/channel
  493. log.Printf("Remove %s from buffer.", msg.MsgParts[3])
  494. Config.DelChannel <- msg.MsgParts[3]
  495. }
  496. if !Config.Registered {
  497. // We're not registered yet
  498. // Answer the queries for SASL authentication
  499. if msg.Cmd == "CAP" && msg.MsgParts[3] == "ACK" {
  500. Config.PriorityWrite("AUTHENTICATE PLAIN")
  501. }
  502. if msg.Cmd == "CAP" && msg.MsgParts[3] == "NAK" {
  503. // SASL Authentication failed/not available.
  504. Config.PriorityWrite("CAP END")
  505. Config.UseSASL = false
  506. }
  507. // msg.Cmd == "AUTH..."
  508. if msg.MsgParts[0] == "AUTHENTICATE" && msg.MsgParts[1] == "+" {
  509. var userpass string = fmt.Sprintf("\x00%s\x00%s", Config.Nick, Config.Password)
  510. var b64 string = base64.StdEncoding.EncodeToString([]byte(userpass))
  511. Config.PriorityWrite("AUTHENTICATE " + b64)
  512. }
  513. if msg.Cmd == "903" {
  514. // Success SASL
  515. Config.PriorityWrite("CAP END")
  516. Config.Registered = true
  517. }
  518. if msg.Cmd == "904" {
  519. // Failed SASL
  520. Config.PriorityWrite("CAP END")
  521. // Should we exit here?
  522. Config.UseSASL = false
  523. }
  524. /*
  525. 2022/04/06 19:12:11 << :[email protected] NOTICE meow :nick, type /msg NickServ IDENTIFY password. Otherwise,
  526. */
  527. if (msg.From == "NickServ") && (msg.Cmd == "NOTICE") {
  528. if strings.Contains(msg.Msg, "IDENTIFY") && Config.Password != "" {
  529. Config.PriorityWrite(fmt.Sprintf("NS IDENTIFY %s", Config.Password))
  530. }
  531. }
  532. if !Config.UseSASL && (msg.Cmd == "900") {
  533. Config.Registered = true
  534. }
  535. }
  536. // This is a better way of knowing when we've identified for services
  537. if (msg.Cmd == "MODE") && (msg.To == Config.MyNick) {
  538. // This should probably be look for + and contains "r"
  539. if (msg.Msg[0] == '+') && (strings.Contains(msg.Msg, "r")) {
  540. Config.ReadChannel <- IRCMsg{Cmd: "Identified"}
  541. if len(Config.AutoJoin) > 0 {
  542. Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
  543. }
  544. }
  545. }
  546. if msg.Cmd == "KICK" {
  547. // We were kicked, is channel in AutoJoin?
  548. if msg.MsgParts[3] == Config.MyNick {
  549. if Config.IsAuto(msg.To) {
  550. // Yes, we were kicked from AutoJoin channel
  551. time.AfterFunc(time.Duration(Config.RejoinDelay)*time.Millisecond, func() { Config.WriteTo(msg.To, "JOIN "+msg.To) })
  552. }
  553. }
  554. }
  555. if Config.ReadChannel != nil {
  556. Config.ReadChannel <- msg
  557. }
  558. if (msg.Cmd == "376") || (msg.Cmd == "422") {
  559. // End MOTD, or MOTD Missing
  560. var reg IRCMsg = IRCMsg{Cmd: "EndMOTD"}
  561. Config.ReadChannel <- reg
  562. registering = true
  563. if Config.Password == "" {
  564. // We can't register with services, so join the AutJoin channels.
  565. if len(Config.AutoJoin) > 0 {
  566. Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
  567. }
  568. }
  569. }
  570. if msg.Cmd == "NICK" {
  571. // Handle nick changes from the server
  572. if Match(msg.From, Config.MyNick) {
  573. Config.SetNick(msg.Msg)
  574. if Config.Debug_Output {
  575. log.Println("Nick is now:", Config.MyNick)
  576. }
  577. }
  578. }
  579. }
  580. }