irc-client.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package ircclient
  2. import (
  3. "bufio"
  4. "crypto/tls"
  5. "crypto/x509"
  6. "encoding/base64"
  7. "fmt"
  8. "io"
  9. "log"
  10. "math/rand"
  11. "net"
  12. "os"
  13. "os/signal"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "syscall"
  18. "time"
  19. )
  20. const VERSION string = "red-green.com/irc-client 0.1.0"
  21. func StrInArray(strings []string, str string) bool {
  22. for _, s := range strings {
  23. if s == str {
  24. return true
  25. }
  26. }
  27. return false
  28. }
  29. type IRCMsg struct {
  30. MsgParts []string
  31. From string
  32. To string
  33. Cmd string
  34. Msg string
  35. }
  36. type IRCWrite struct {
  37. To string
  38. Output string
  39. }
  40. type IRCConfig struct {
  41. Port int `json:"Port"`
  42. Hostname string `json:"Hostname"`
  43. UseTLS bool `json:"UseTLS"` // Use TLS Secure connection
  44. UseSASL bool `json:"UseSASL"` // Authenticate via SASL
  45. Insecure bool `json:"Insecure"` // Allow self-signed certificates
  46. Nick string `json:"Nick"`
  47. Username string `json:"Username"`
  48. Realname string `json:"Realname"`
  49. Password string `json:"Password"` // Password for nickserv
  50. ServerPassword string `json:"ServerPassword"` // Password for server
  51. AutoJoin []string `json:"AutoJoin"` // Channels to auto-join
  52. RejoinDelay int `json:"RejoinDelay"` // ms to rejoin
  53. Version string `json:"Version"` // Version displayed
  54. Flood_Num int `json:"FloodNum"` // Number of lines sent before considered a flood
  55. Flood_Time int `json:"FloodTime"` // Number of Seconds to track previous messages
  56. Flood_Delay int `json:"FloodDelay"` // Delay between sending when flood protection on (Milliseconds)
  57. Debug_Output bool `json:"Debug"`
  58. MyNick string
  59. OnExit func()
  60. Socket net.Conn
  61. Reader *bufio.Reader
  62. ReadChannel chan IRCMsg
  63. ReadEvents []string
  64. WriteChannel chan IRCWrite
  65. DelChannel chan string
  66. Registered bool
  67. ISupport map[string]string // 005
  68. wg sync.WaitGroup
  69. Mutex sync.Mutex
  70. }
  71. func (Config *IRCConfig) GetNick() string {
  72. Config.Mutex.Lock()
  73. defer Config.Mutex.Unlock()
  74. return Config.MyNick
  75. }
  76. func (Config *IRCConfig) SetNick(nick string) {
  77. Config.Mutex.Lock()
  78. defer Config.Mutex.Unlock()
  79. Config.MyNick = nick
  80. }
  81. // Writer *bufio.Writer
  82. func (Config *IRCConfig) IsAuto(ch string) bool {
  83. return StrInArray(Config.AutoJoin, ch)
  84. }
  85. func (Config *IRCConfig) Connect() bool {
  86. var err error
  87. if Config.Flood_Num == 0 {
  88. Config.Flood_Num = 5
  89. }
  90. if Config.Flood_Time == 0 {
  91. Config.Flood_Time = 10
  92. }
  93. if Config.Flood_Delay == 0 {
  94. Config.Flood_Delay = 1000
  95. }
  96. if Config.UseSASL {
  97. if !Config.UseTLS {
  98. log.Println("Can't UseSASL if not using UseTLS")
  99. Config.UseSASL = false
  100. }
  101. }
  102. Config.Registered = false
  103. if Config.ReadChannel == nil {
  104. log.Println("Warning: ReadChannel is nil.")
  105. }
  106. if Config.UseTLS {
  107. var tlsConfig tls.Config
  108. if !Config.Insecure {
  109. certPool := x509.NewCertPool()
  110. tlsConfig.ClientCAs = certPool
  111. tlsConfig.InsecureSkipVerify = false
  112. } else {
  113. tlsConfig.InsecureSkipVerify = true
  114. }
  115. Config.Socket, err = tls.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port), &tlsConfig)
  116. if err != nil {
  117. log.Fatal("tls.Dial:", err)
  118. }
  119. // Config.Writer = bufio.NewWriter(Config.TLSSocket)
  120. Config.Reader = bufio.NewReader(Config.Socket)
  121. } else {
  122. Config.Socket, err = net.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port))
  123. if err != nil {
  124. log.Fatal("net.Dial:", err)
  125. }
  126. // Config.Writer = bufio.NewWriter(Config.Socket)
  127. Config.Reader = bufio.NewReader(Config.Socket)
  128. }
  129. // WriteChannel may contain a message when we're trying to PriorityWrite from sigChannel.
  130. Config.WriteChannel = make(chan IRCWrite, 3)
  131. Config.DelChannel = make(chan string)
  132. Config.ISupport = make(map[string]string)
  133. // We are connected.
  134. go Config.WriterRoutine()
  135. Config.wg.Add(1)
  136. // Registration
  137. if Config.UseTLS && Config.UseSASL {
  138. Config.PriorityWrite("CAP REQ :sasl")
  139. }
  140. if Config.ServerPassword != "" {
  141. Config.PriorityWrite(fmt.Sprintf("PASS %s", Config.ServerPassword))
  142. }
  143. // Register nick
  144. Config.MyNick = Config.Nick
  145. Config.PriorityWrite(fmt.Sprintf("NICK %s", Config.Nick))
  146. Config.PriorityWrite(fmt.Sprintf("USER %s 0 * :%s", Config.Username, Config.Realname))
  147. // Config.Writer.Flush()
  148. go Config.ReaderRoutine()
  149. Config.wg.Add(1)
  150. return true
  151. }
  152. // Low level write to [TLS]Socket routine.
  153. func (Config *IRCConfig) write(output string) error {
  154. var err error
  155. if Config.Debug_Output {
  156. log.Println(">>", output)
  157. }
  158. output += "\r\n"
  159. /*
  160. if Config.UseTLS {
  161. _, err = Config.TLSSocket.Write([]byte(output))
  162. } else {
  163. */
  164. _, err = Config.Socket.Write([]byte(output))
  165. return err
  166. }
  167. func (Config *IRCConfig) WriterRoutine() {
  168. var err error
  169. var throttle ThrottleBuffer
  170. var Flood FloodTrack
  171. defer Config.wg.Done()
  172. throttle.init()
  173. Flood.Init(Config.Flood_Num, Config.Flood_Time)
  174. // signal handler
  175. var sigChannel chan os.Signal = make(chan os.Signal, 1)
  176. signal.Notify(sigChannel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  177. // Change this into a select with timeout.
  178. // Timeout, if there's something to be buffered.
  179. var gotSignal bool
  180. for {
  181. if throttle.Life_sucks {
  182. select {
  183. case output := <-Config.WriteChannel:
  184. if output.To == "" {
  185. err = Config.write(output.Output)
  186. if err != nil {
  187. log.Println("Writer:", err)
  188. return
  189. }
  190. continue
  191. }
  192. throttle.push(output.To, output.Output)
  193. case <-sigChannel:
  194. if !gotSignal {
  195. gotSignal = true
  196. log.Println("SIGNAL")
  197. /*
  198. // This should be handled now by Close().
  199. if Config.OnExit != nil {
  200. Config.OnExit()
  201. }
  202. */
  203. Config.PriorityWrite("QUIT :Received SIGINT")
  204. }
  205. // return
  206. continue
  207. // Config.Close()
  208. // return
  209. //os.Exit(2)
  210. case remove := <-Config.DelChannel:
  211. if Config.Debug_Output {
  212. log.Printf("Remove: [%s]\n", remove)
  213. }
  214. throttle.delete(remove)
  215. case <-time.After(time.Millisecond * time.Duration(Config.Flood_Delay)):
  216. // Send from the buffer
  217. // debugging the flood buffer
  218. // log.Printf("targets: %#v\n", targets)
  219. // log.Printf("last: %d\n", last)
  220. // log.Printf("buffer: %#v\n", buffer)
  221. var msg string = throttle.pop()
  222. err = Config.write(msg)
  223. if err != nil {
  224. log.Println("Writer:", err)
  225. }
  226. }
  227. } else {
  228. // Life is good.
  229. select {
  230. case <-sigChannel:
  231. if !gotSignal {
  232. gotSignal = true
  233. log.Println("SIGNAL")
  234. /*
  235. // This should now be handled by Close().
  236. if Config.OnExit != nil {
  237. Config.OnExit()
  238. }
  239. */
  240. Config.PriorityWrite("QUIT :Received SIGINT")
  241. }
  242. // return
  243. continue
  244. // Config.Close()
  245. // return
  246. // os.Exit(2)
  247. case remove := <-Config.DelChannel:
  248. if Config.Debug_Output {
  249. log.Printf("Remove: [%s]\n", remove)
  250. }
  251. throttle.delete(remove)
  252. case output := <-Config.WriteChannel:
  253. if output.To == "" {
  254. err = Config.write(output.Output)
  255. if err != nil {
  256. log.Println("Writer:", err)
  257. return
  258. }
  259. continue
  260. }
  261. if Flood.Full() {
  262. throttle.push(output.To, output.Output)
  263. } else {
  264. // Flood limits not reached
  265. Flood.Save()
  266. err = Config.write(output.Output)
  267. if err != nil {
  268. log.Println("Writer:", err)
  269. }
  270. }
  271. }
  272. }
  273. }
  274. log.Println("~WriterRoutine")
  275. }
  276. func (Config *IRCConfig) Close() {
  277. Config.Socket.Close()
  278. Config.PriorityWrite("")
  279. if Config.OnExit != nil {
  280. Config.OnExit()
  281. }
  282. Config.wg.Wait()
  283. }
  284. func IRCParse(line string) []string {
  285. var pos int = strings.Index(line, " :")
  286. var message string
  287. if pos != -1 {
  288. message = line[pos+2:]
  289. line = line[:pos]
  290. }
  291. var results []string
  292. results = strings.Split(line, " ")
  293. if message != "" {
  294. results = append(results, message)
  295. }
  296. return results
  297. }
  298. func IRCNick(from string) string {
  299. if from[0] == ':' {
  300. from = from[1:]
  301. }
  302. var pos int = strings.Index(from, "!")
  303. if pos != -1 {
  304. from = from[:pos]
  305. }
  306. return from
  307. }
  308. func RandomNick(nick string) string {
  309. var result string = nick + "-"
  310. result += strconv.Itoa(rand.Intn(1000))
  311. return result
  312. }
  313. func (Config *IRCConfig) PriorityWrite(output string) {
  314. Config.WriteChannel <- IRCWrite{To: "", Output: output}
  315. }
  316. func (Config *IRCConfig) WriteTo(to string, output string) {
  317. Config.WriteChannel <- IRCWrite{To: to, Output: output}
  318. }
  319. func (Config *IRCConfig) Msg(to string, message string) {
  320. Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :%s", to, message))
  321. }
  322. func (Config *IRCConfig) Notice(to string, message string) {
  323. Config.WriteTo(to, fmt.Sprintf("NOTICE %s :%s", to, message))
  324. }
  325. func (Config *IRCConfig) Action(to string, message string) {
  326. Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :\x01ACTION %s\x01", to, message))
  327. }
  328. func (Config *IRCConfig) ReaderRoutine() {
  329. defer Config.wg.Done()
  330. var registering bool
  331. // var identified bool
  332. for {
  333. var line string
  334. var err error
  335. var results []string
  336. line, err = Config.Reader.ReadString('\n')
  337. if err == nil {
  338. line = strings.Trim(line, "\r\n")
  339. if Config.Debug_Output {
  340. log.Println("<<", line)
  341. }
  342. results = IRCParse(line)
  343. if results[1] == "433" && !registering {
  344. // Nick already in use!
  345. var newNick string = RandomNick(Config.Nick)
  346. Config.MyNick = newNick
  347. Config.PriorityWrite("NICK " + newNick)
  348. }
  349. if results[1] == "PRIVMSG" {
  350. // Is this an action?
  351. if len(results) >= 3 {
  352. if (results[3][0] == '\x01') && (results[3][len(results[3])-1] == '\x01') {
  353. // ACTION
  354. results[1] = "CTCP"
  355. results[3] = results[3][1 : len(results[3])-1]
  356. log.Println("CTCP:", results[3])
  357. // Process CTCP commands
  358. if strings.HasPrefix(results[3], "ACTION ") {
  359. results[1] = "ACTION"
  360. results[3] = results[3][7:]
  361. }
  362. if strings.HasPrefix(results[3], "PING ") {
  363. Config.WriteTo(IRCNick(results[0]),
  364. fmt.Sprintf("NOTICE %s :\x01PING %s\x01",
  365. IRCNick(results[0]),
  366. results[3][5:]))
  367. }
  368. if results[3] == "VERSION" {
  369. // Send version reply
  370. var version string
  371. if Config.Version != "" {
  372. version = Config.Version + " " + VERSION
  373. } else {
  374. version = VERSION
  375. }
  376. Config.WriteTo(IRCNick(results[0]),
  377. fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01",
  378. IRCNick(results[0]), version))
  379. }
  380. if results[3] == "TIME" {
  381. // Send time reply
  382. var now time.Time = time.Now()
  383. Config.WriteTo(IRCNick(results[0]),
  384. fmt.Sprintf("NOTICE %s :\x01TIME %s\x01",
  385. IRCNick(results[0]),
  386. now.Format(time.ANSIC)))
  387. }
  388. }
  389. }
  390. }
  391. } else {
  392. // This is likely, 2022/04/05 10:11:41 ReadString: EOF
  393. if err != io.EOF {
  394. log.Println("ReadString:", err)
  395. }
  396. close(Config.ReadChannel)
  397. return
  398. }
  399. var msg IRCMsg = IRCMsg{MsgParts: results}
  400. if len(results) >= 3 {
  401. msg.From = IRCNick(results[0])
  402. msg.Cmd = results[1]
  403. msg.To = results[2]
  404. if len(results) >= 4 {
  405. msg.Msg = results[3]
  406. }
  407. } else {
  408. msg.Cmd = results[0]
  409. }
  410. if !Config.Debug_Output {
  411. if msg.Cmd == "ERROR" || msg.Cmd[0] == '4' || msg.Cmd[0] == '5' {
  412. // Always log errors.
  413. log.Println("<<", line)
  414. }
  415. }
  416. // Answer PING/PONG immediately.
  417. if results[0] == "PING" {
  418. Config.PriorityWrite("PONG " + results[1])
  419. }
  420. if (msg.Cmd == "401") || (msg.Cmd == "404") {
  421. // No such nick/channel
  422. log.Printf("Remove %s from buffer.", msg.MsgParts[3])
  423. Config.DelChannel <- msg.MsgParts[3]
  424. }
  425. if msg.Cmd == "005" {
  426. // ISUPPORT msg.MsgParts[3:len(msg.MsgParts)-1]
  427. var support string
  428. for _, support = range msg.MsgParts[3 : len(msg.MsgParts)-1] {
  429. if strings.Contains(support, "=") {
  430. var suppart []string = strings.Split(support, "=")
  431. Config.ISupport[suppart[0]] = suppart[1]
  432. } else {
  433. Config.ISupport[support] = ""
  434. }
  435. }
  436. }
  437. if !Config.Registered {
  438. // We're not registered yet
  439. // Answer the queries for SASL authentication
  440. if (msg.Cmd == "CAP") && (msg.Msg == "ACK") {
  441. Config.PriorityWrite("AUTHENTICATE PLAIN")
  442. }
  443. if msg.Cmd == "CAP" && msg.Msg == "NAK" {
  444. // SASL Authentication failed/not available.
  445. Config.PriorityWrite("CAP END")
  446. Config.UseSASL = false
  447. }
  448. if (msg.MsgParts[0] == "AUTHENTICATE") && (msg.MsgParts[1] == "+") {
  449. var userpass string = fmt.Sprintf("\x00%s\x00%s", Config.Nick, Config.Password)
  450. var b64 string = base64.StdEncoding.EncodeToString([]byte(userpass))
  451. Config.PriorityWrite("AUTHENTICATE " + b64)
  452. }
  453. if msg.Cmd == "903" {
  454. // Success SASL
  455. Config.PriorityWrite("CAP END")
  456. Config.Registered = true
  457. }
  458. if msg.Cmd == "904" {
  459. // Failed SASL
  460. Config.PriorityWrite("CAP END")
  461. // Should we exit here?
  462. Config.UseSASL = false
  463. }
  464. /*
  465. 2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :This nickname is registered and protected. If it is your
  466. 2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :nick, type /msg NickServ IDENTIFY password. Otherwise,
  467. 2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :please choose a different nick.
  468. */
  469. if (msg.From == "NickServ") && (msg.Cmd == "NOTICE") {
  470. if strings.Contains(msg.Msg, "IDENTIFY") && Config.Password != "" {
  471. Config.PriorityWrite(fmt.Sprintf("NS IDENTIFY %s", Config.Password))
  472. }
  473. // :NickServ!services@services.red-green.com NOTICE meow :Password accepted - you are now recognized.
  474. }
  475. if !Config.UseSASL && (msg.Cmd == "900") {
  476. Config.Registered = true
  477. }
  478. }
  479. // This is a better way of knowing when we've identified for services
  480. if (msg.Cmd == "MODE") && (msg.To == Config.MyNick) {
  481. // This should probably be look for + and contains "r"
  482. if (msg.Msg[0] == '+') && (strings.Contains(msg.Msg, "r")) {
  483. Config.ReadChannel <- IRCMsg{Cmd: "Identified"}
  484. // identified = true
  485. }
  486. if len(Config.AutoJoin) > 0 {
  487. Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
  488. }
  489. }
  490. if msg.Cmd == "KICK" {
  491. // Were we kicked, is channel in AutoJoin?
  492. // 2022/04/13 20:02:52 << :bugz!bugz@furball.red-green.com KICK #bugz meow-bot :bugz
  493. // Msg: ircclient.IRCMsg{MsgParts:[]string{":bugz!bugz@furball.red-green.com", "KICK", "#bugz", "meow-bot", "bugz"}, From:"bugz", To:"#bugz", Cmd:"KICK", Msg:"meow-bot"}
  494. if strings.Contains(msg.Msg, Config.MyNick) {
  495. if Config.IsAuto(msg.To) {
  496. // Yes, we were kicked from AutoJoin channel
  497. time.AfterFunc(time.Duration(Config.RejoinDelay)*time.Millisecond, func() { Config.WriteTo(msg.To, "JOIN "+msg.To) })
  498. }
  499. }
  500. }
  501. /*
  502. // Needs rate limit, it doesn't always work. (if they're not on the HOP+ list)
  503. if msg.Cmd == "474" && identified {
  504. :irc.red-green.com 474 meow-bot #chat :Cannot join channel (+b)
  505. Msg: ircclient.IRCMsg{MsgParts:[]string{":irc.red-green.com", "474", "meow-bot", "#chat", "Cannot join channel (+b)"}, From:"irc.red-green.com", To:"meow-bot", Cmd:"474", Msg:"#chat"}
  506. :ChanServ!services@services.red-green.com NOTICE meow-bot :Access denied.
  507. Msg: ircclient.IRCMsg{MsgParts:[]string{":ChanServ!services@services.red-green.com", "NOTICE", "meow-bot", "Access denied."}, From:"ChanServ", To:"meow-bot", Cmd:"NOTICE", Msg:"Access denied."}
  508. if Config.IsAuto(msg.MsgParts[3]) {
  509. Config.PriorityWrite(fmt.Sprintf("CS UNBAN %s", msg.MsgParts[3]))
  510. time.AfterFunc(time.Duration(Config.RejoinDelay)*time.Millisecond, func() { Config.WriteTo(msg.To, "JOIN "+msg.MsgParts[3]) })
  511. }
  512. }
  513. */
  514. if Config.ReadChannel != nil {
  515. Config.ReadChannel <- msg
  516. }
  517. if (msg.Cmd == "376") || (msg.Cmd == "422") {
  518. // End MOTD, or MOTD Missing
  519. var reg IRCMsg = IRCMsg{Cmd: "EndMOTD"}
  520. Config.ReadChannel <- reg
  521. registering = true
  522. }
  523. if msg.Cmd == "NICK" {
  524. // :meow NICK :meow-bot
  525. if msg.From == Config.MyNick {
  526. Config.SetNick(msg.To)
  527. log.Println("Nick is now:", Config.MyNick)
  528. }
  529. }
  530. }
  531. }