main.go (32464B)
1 /* 2 * Copyright (c) 2025, Natacha Porté 3 * 4 * Permission to use, copy, modify, and distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 package main 18 19 import ( 20 "database/sql" 21 "embed" 22 "encoding/base64" 23 "encoding/hex" 24 "errors" 25 "fmt" 26 "log" 27 "os" 28 "regexp" 29 "runtime/debug" 30 "strconv" 31 "strings" 32 "sync/atomic" 33 "time" 34 35 "github.com/dustin/go-humanize" 36 _ "github.com/glebarez/go-sqlite" 37 "github.com/nats-io/nats.go" 38 "github.com/pelletier/go-toml/v2" 39 "github.com/thoj/go-ircevent" 40 ) 41 42 type command struct { 43 name string 44 arg string 45 } 46 47 func main() { 48 setVersion() 49 50 config_file := "natsim.toml" 51 if len(os.Args) > 1 { 52 config_file = os.Args[1] 53 } 54 55 im, err := NewNatsIM(config_file) 56 if err != nil { 57 log.Fatal(err) 58 } 59 60 log.Println("natsim " + version + " started") 61 62 im.irc.Loop() 63 } 64 65 /**************** Construction ****************/ 66 67 type LineMark struct { 68 Start string 69 Mid string 70 End string 71 } 72 73 type IrcConfig struct { 74 Channel string 75 Server string 76 Nick string 77 Cmd LineMark 78 Send LineMark 79 Show LineMark 80 ShowReply *LineMark 81 ShowHeader *LineMark 82 MaxLine int 83 ContSuffix string 84 ContPrefix string 85 AntiFlood antiflood 86 Filter []FilterElement 87 AutoClear bool 88 nextClear bool 89 AllowCmd []string 90 AllowSend []string 91 BlockCmd []string 92 BlockSend []string 93 MaxQuoteRatio float32 94 MaxBase64 int 95 MaxHex int 96 } 97 98 type LogConfig struct { 99 SqlDriver string 100 SqlConnection string 101 Filter []FilterElement 102 } 103 104 type NatsConfig struct { 105 Name string 106 Server string 107 NkeySeed string 108 Subjects []string 109 Filter []FilterElement 110 RetryOnFailedConnect bool 111 } 112 113 type NatsIM struct { 114 Irc IrcConfig 115 Log LogConfig 116 Nats NatsConfig 117 118 irc *irc.Connection 119 nc *nats.Conn 120 subs []*nats.Subscription 121 curMsg nats.Msg 122 db *sql.DB 123 ensureSubject *sql.Stmt 124 insertReceived *sql.Stmt 125 insertRHeader *sql.Stmt 126 insertSent *sql.Stmt 127 insertSHeader *sql.Stmt 128 cmdQueue chan command 129 ircQueue chan string 130 dropped atomic.Uint32 131 } 132 133 func NewNatsIM(configPath string) (*NatsIM, error) { 134 natsim := &NatsIM{ 135 Irc: IrcConfig{ 136 Nick: "natsim", 137 Cmd: LineMark{Start: "!", Mid: " "}, 138 Send: LineMark{Mid: ": "}, 139 Show: LineMark{Mid: ": "}, 140 AutoClear: true, 141 MaxQuoteRatio: 2.0, 142 MaxBase64: 256, 143 MaxHex: 64, 144 }, 145 Nats: NatsConfig{ 146 Name: "nastim", 147 Subjects: []string{">"}, 148 }, 149 } 150 151 f, err := os.Open(configPath) 152 if err != nil { 153 return nil, err 154 } 155 defer func() { 156 if err := f.Close(); err != nil { 157 log.Println("Closing configuration", configPath, err) 158 } 159 }() 160 161 d := toml.NewDecoder(f).DisallowUnknownFields() 162 err = d.Decode(natsim) 163 if err != nil { 164 var details *toml.StrictMissingError 165 if errors.As(err, &details) { 166 for _, missing := range details.Errors { 167 log.Println(missing) 168 } 169 } 170 return nil, err 171 } 172 173 if natsim.Irc.MaxLine > 0 && len(natsim.Irc.ContPrefix)+len(natsim.Irc.ContSuffix) >= natsim.Irc.MaxLine { 174 natsim.Irc.ContPrefix = "" 175 natsim.Irc.ContSuffix = "" 176 } 177 178 if natsim.Log.SqlDriver != "" { 179 if err := natsim.logInit(); err != nil { 180 natsim.Close() 181 return nil, err 182 } 183 } 184 185 natsim.Irc.nextClear = natsim.Irc.AutoClear 186 187 natsim.cmdQueue = make(chan command, 10) 188 natsim.ircQueue = make(chan string, 10) 189 190 natsim.irc = irc.IRC(natsim.Irc.Nick, "natsim") 191 natsim.irc.AddCallback("001", natsim.ircJoin) 192 natsim.irc.AddCallback("366", natsim.ircJoined) 193 natsim.irc.AddCallback("PRIVMSG", natsim.ircReceive) 194 195 err = natsim.irc.Connect(natsim.Irc.Server) 196 if err != nil { 197 natsim.irc = nil 198 natsim.Close() 199 return nil, err 200 } 201 202 go natsim.doCommands() 203 go natsim.ircSender() 204 205 return natsim, nil 206 } 207 208 func (natsim *NatsIM) Close() { 209 if natsim.irc != nil { 210 natsim.irc.Quit() 211 natsim.irc = nil 212 } 213 214 if natsim.nc != nil { 215 natsim.nc.Close() 216 natsim.nc = nil 217 } 218 219 if natsim.ensureSubject != nil { 220 if err := natsim.ensureSubject.Close(); err != nil { 221 log.Println("Close ensureSubject:", err) 222 } 223 natsim.ensureSubject = nil 224 } 225 226 if natsim.insertReceived != nil { 227 if err := natsim.insertReceived.Close(); err != nil { 228 log.Println("Close insertReceived:", err) 229 } 230 natsim.insertReceived = nil 231 } 232 233 if natsim.insertRHeader != nil { 234 if err := natsim.insertRHeader.Close(); err != nil { 235 log.Println("Close insertRHeader:", err) 236 } 237 natsim.insertRHeader = nil 238 } 239 240 if natsim.insertSent != nil { 241 if err := natsim.insertSent.Close(); err != nil { 242 log.Println("Close insertSent:", err) 243 } 244 natsim.insertSent = nil 245 } 246 247 if natsim.insertSHeader != nil { 248 if err := natsim.insertSHeader.Close(); err != nil { 249 log.Println("Close insertSHeader:", err) 250 } 251 natsim.insertSHeader = nil 252 } 253 254 if natsim.db != nil { 255 if err := natsim.db.Close(); err != nil { 256 log.Println("Close log DB:", err) 257 } 258 natsim.db = nil 259 } 260 261 close(natsim.cmdQueue) 262 close(natsim.ircQueue) 263 } 264 265 /**************** Command Goroutine ****************/ 266 267 func (natsim *NatsIM) doCommands() { 268 for { 269 cmd, ok := <-natsim.cmdQueue 270 if !ok { 271 return 272 } 273 274 switch cmd.name { 275 case "allowcmd": 276 updateNickList(cmd.arg, &natsim.Irc.AllowCmd, &natsim.Irc.BlockCmd) 277 natsim.ircSendf("%s - %s", strNickList(natsim.Irc.AllowCmd), strNickList(natsim.Irc.BlockCmd)) 278 279 case "allowsend": 280 updateNickList(cmd.arg, &natsim.Irc.AllowSend, &natsim.Irc.BlockSend) 281 natsim.ircSendf("%s - %s", strNickList(natsim.Irc.AllowSend), strNickList(natsim.Irc.BlockSend)) 282 283 case "autoclear": 284 switch cmd.arg { 285 case "": 286 temp := "" 287 state := "off" 288 if natsim.Irc.AutoClear != natsim.Irc.nextClear { 289 temp = "temporarily " 290 } 291 if natsim.Irc.AutoClear { 292 state = "on" 293 } 294 natsim.ircSendf("Autoclear is %s%s", temp, state) 295 case "after": 296 natsim.Irc.AutoClear = false 297 natsim.Irc.nextClear = true 298 case "on": 299 natsim.Irc.AutoClear = true 300 natsim.Irc.nextClear = true 301 case "once": 302 natsim.Irc.AutoClear = true 303 natsim.Irc.nextClear = false 304 case "off": 305 natsim.Irc.AutoClear = false 306 natsim.Irc.nextClear = false 307 default: 308 natsim.ircSendf("Unknown autoclear option %q", cmd.arg) 309 } 310 311 case "b64data": 312 if decoded, err := b64Decode(cmd.arg); err != nil { 313 natsim.ircSendError("b64Decode", err) 314 } else { 315 natsim.curMsg.Data = append(natsim.curMsg.Data, []byte(decoded)...) 316 } 317 318 case "blockcmd": 319 updateNickList(cmd.arg, &natsim.Irc.BlockCmd, &natsim.Irc.AllowCmd) 320 natsim.ircSendf("%s - %s", strNickList(natsim.Irc.BlockCmd), strNickList(natsim.Irc.AllowCmd)) 321 322 case "blocksend": 323 updateNickList(cmd.arg, &natsim.Irc.BlockSend, &natsim.Irc.AllowSend) 324 natsim.ircSendf("%s - %s", strNickList(natsim.Irc.BlockSend), strNickList(natsim.Irc.AllowSend)) 325 326 case "cleardata": 327 natsim.curMsg.Data = []byte{} 328 329 case "clearmsg": 330 natsim.curMsg = nats.Msg{} 331 332 case "curmsg": 333 var sb strings.Builder 334 sb.WriteString("[WIP]") 335 sb.WriteString(packMark(natsim.Irc.Show, 336 natsim.curMsg.Subject, 337 natsim.ircQuoteData(natsim.curMsg.Data))) 338 339 if natsim.curMsg.Reply != "" { 340 show := LineMark{Start: "Reply-To:"} 341 if natsim.Irc.ShowReply != nil { 342 show = *natsim.Irc.ShowReply 343 } 344 sb.WriteString(show.Start) 345 sb.WriteString(natsim.curMsg.Reply) 346 sb.WriteString(show.End) 347 } 348 349 show := LineMark{Mid: ": "} 350 if natsim.Irc.ShowHeader != nil { 351 show = *natsim.Irc.ShowHeader 352 } 353 for key, values := range natsim.curMsg.Header { 354 for _, value := range values { 355 sb.WriteString(packMark(show, key, value)) 356 } 357 } 358 359 natsim.ircSend(sb.String()) 360 361 case "delheader": 362 index := -1 363 key := cmd.arg 364 365 if strings.HasPrefix(cmd.arg, "* ") { 366 key = key[2:] 367 } else if before, after, found := strings.Cut(cmd.arg, " "); found { 368 if n, err := strconv.Atoi(before); err == nil { 369 index = n 370 key = after 371 } 372 } 373 374 if natsim.curMsg.Header == nil || natsim.curMsg.Header[key] == nil { 375 natsim.ircSendf("No recorded header %q", key) 376 } else if index < 0 { 377 delete(natsim.curMsg.Header, key) 378 } else if index < len(natsim.curMsg.Header[key]) { 379 natsim.curMsg.Header[key] = append(natsim.curMsg.Header[key][:index], natsim.curMsg.Header[key][index+1:]...) 380 } else { 381 natsim.ircSendf("No index %d in header %q", index, key) 382 } 383 384 case "data": 385 natsim.curMsg.Data = append(natsim.curMsg.Data, []byte(cmd.arg)...) 386 387 case "filter": 388 var plist *[]FilterElement 389 var name string 390 index := -1 391 place, eltstr, _ := strings.Cut(cmd.arg, " ") 392 uplace := strings.ToUpper(place) 393 394 if uplace == "NATS" { 395 plist = &natsim.Nats.Filter 396 name = "N" 397 } else if uplace == "LOG" { 398 plist = &natsim.Log.Filter 399 name = "L" 400 } else if uplace == "IRC" { 401 plist = &natsim.Irc.Filter 402 name = "I" 403 } else if n, err := strconv.Atoi(uplace[1:]); err == nil && (uplace[0:1] == "N" || uplace[0:1] == "L" || uplace[0:1] == "I") { 404 index = n - 1 405 name = uplace[0:1] 406 switch name { 407 case "N": 408 plist = &natsim.Nats.Filter 409 case "L": 410 plist = &natsim.Log.Filter 411 case "I": 412 plist = &natsim.Irc.Filter 413 } 414 } else { 415 natsim.ircSendf("Unable to parse place %q", uplace) 416 } 417 418 var elt FilterElement 419 if plist != nil { 420 err := elt.UnmarshalText([]byte(eltstr)) 421 if err != nil { 422 natsim.ircSendError("Parse FilterElement", err) 423 plist = nil 424 } 425 } 426 427 if plist != nil { 428 if index < 0 { 429 index += len(*plist) + 1 430 } 431 432 if index >= 0 && index < len(*plist) { 433 *plist = append((*plist)[:index+1], (*plist)[index:]...) 434 (*plist)[index] = elt 435 } else { 436 index = len(*plist) 437 *plist = append(*plist, elt) 438 } 439 natsim.ircSendf("Inserted filter element %s%d/%d", name, index+1, len(*plist)) 440 } 441 442 case "filters": 443 var buf strings.Builder 444 buf.WriteString(fmt.Sprintf("Active filters: %d NATS, %d log, %d IRC", len(natsim.Nats.Filter), len(natsim.Log.Filter), len(natsim.Irc.Filter))) 445 WriteFilter(&buf, "\n N", natsim.Nats.Filter) 446 WriteFilter(&buf, "\n L", natsim.Log.Filter) 447 WriteFilter(&buf, "\n I", natsim.Irc.Filter) 448 natsim.ircSend(buf.String()) 449 450 case "hdata": 451 if decoded, err := hexDecode(cmd.arg); err != nil { 452 natsim.ircSendError("hexDecode", err) 453 } else { 454 natsim.curMsg.Data = append(natsim.curMsg.Data, []byte(decoded)...) 455 } 456 457 case "header": 458 sep := ": " 459 if natsim.Irc.ShowHeader != nil { 460 sep = natsim.Irc.ShowHeader.Mid 461 } 462 if key, value, found := strings.Cut(cmd.arg, sep); !found { 463 natsim.ircSendf("No header separator %q", sep) 464 } else { 465 if natsim.curMsg.Header == nil { 466 natsim.curMsg.Header = make(nats.Header) 467 } 468 natsim.curMsg.Header[key] = append(natsim.curMsg.Header[key], value) 469 } 470 471 case "max-base64": 472 fallthrough 473 case "maxbase64": 474 if val, err := strconv.Atoi(cmd.arg); err != nil { 475 natsim.ircSendError("Parse MaxBase64", err) 476 } else { 477 natsim.Irc.MaxBase64 = val 478 } 479 480 case "max-hex": 481 fallthrough 482 case "maxhex": 483 if val, err := strconv.Atoi(cmd.arg); err != nil { 484 natsim.ircSendError("Parse MaxHex", err) 485 } else { 486 natsim.Irc.MaxHex = val 487 } 488 489 case "max-quote-ratio": 490 fallthrough 491 case "maxquoteratio": 492 if val, err := strconv.ParseFloat(cmd.arg, 32); err != nil { 493 natsim.ircSendError("Parse MaxQuoteRatio", err) 494 } else { 495 natsim.Irc.MaxQuoteRatio = float32(val) 496 } 497 498 case "new-inbox": 499 fallthrough 500 case "newinbox": 501 inbox := natsim.nc.NewInbox() 502 natsim.curMsg.Reply = inbox 503 natsim.ircSendf("Reply-To: %q", inbox) 504 505 case "qdata": 506 if unquoted, err := strconv.Unquote(cmd.arg); err != nil { 507 natsim.ircSendError("Unquote", err) 508 } else { 509 natsim.curMsg.Data = append(natsim.curMsg.Data, []byte(unquoted)...) 510 } 511 512 case "quit": 513 log.Println("Quit command", cmd.arg) 514 natsim.irc.QuitMessage = cmd.arg 515 natsim.Close() 516 517 case "reply-to": 518 fallthrough 519 case "replyto": 520 natsim.curMsg.Reply = cmd.arg 521 522 case "send": 523 if natsim.curMsg.Subject == "" { 524 natsim.ircSend("Cannot send message without subject") 525 } else if err := natsim.nc.PublishMsg(&natsim.curMsg); err != nil { 526 natsim.ircSendError("Publish", err) 527 } else { 528 natsim.logSent(&natsim.curMsg) 529 } 530 if natsim.Irc.AutoClear { 531 natsim.curMsg = nats.Msg{} 532 } 533 natsim.Irc.AutoClear = natsim.Irc.nextClear 534 535 case "status": 536 var buf strings.Builder 537 538 if err := natsim.nc.LastError(); err != nil { 539 buf.WriteString("Last error: ") 540 buf.WriteString(err.Error()) 541 buf.WriteString("\n") 542 } 543 544 buf.WriteString(natsim.nc.Status().String()) 545 546 if url := natsim.nc.ConnectedUrlRedacted(); url != "" { 547 buf.WriteString(" to ") 548 buf.WriteString(url) 549 } 550 551 if rtt, err := natsim.nc.RTT(); err == nil { 552 buf.WriteString(", RTT ") 553 buf.WriteString(rtt.String()) 554 } 555 556 buf.WriteString(fmt.Sprintf(", %d subscriptions\n%s", natsim.nc.NumSubscriptions(), natsim.natsStats())) 557 natsim.ircSend(buf.String()) 558 559 case "subject": 560 natsim.curMsg.Subject = cmd.arg 561 562 case "subscribe": 563 if s, err := natsim.nc.Subscribe(cmd.arg, natsim.natsReceive); err != nil { 564 natsim.ircSendError("Subscribe", err) 565 } else { 566 natsim.subs = append(natsim.subs, s) 567 natsim.ircSendf("Subscribed to %q", s.Subject) 568 } 569 570 case "subscriptions": 571 var buf strings.Builder 572 buf.WriteString(fmt.Sprintf("Current subscriptions (%d):", len(natsim.subs))) 573 for i, s := range natsim.subs { 574 buf.WriteString(fmt.Sprintf("\n%d. %s", i+1, s.Subject)) 575 } 576 natsim.ircSend(buf.String()) 577 578 case "unfilter": 579 uplace := strings.ToUpper(cmd.arg) 580 581 if uplace == "NATS" { 582 n := len(natsim.Nats.Filter) 583 natsim.Nats.Filter = []FilterElement{} 584 natsim.ircSendf("Removed %d NATS filter elements", n) 585 } else if uplace == "LOG" { 586 n := len(natsim.Log.Filter) 587 natsim.Log.Filter = []FilterElement{} 588 natsim.ircSendf("Removed %d log filter elements", n) 589 } else if uplace == "IRC" { 590 n := len(natsim.Irc.Filter) 591 natsim.Irc.Filter = []FilterElement{} 592 natsim.ircSendf("Removed %d IRC filter elements", n) 593 } else if n, err := strconv.Atoi(uplace[1:]); err == nil && (uplace[0:1] == "N" || uplace[0:1] == "L" || uplace[0:1] == "I") { 594 var plist *[]FilterElement 595 index := n - 1 596 name := uplace[0:1] 597 switch name { 598 case "N": 599 plist = &natsim.Nats.Filter 600 case "L": 601 plist = &natsim.Log.Filter 602 case "I": 603 plist = &natsim.Irc.Filter 604 } 605 if n < 0 { 606 index = len(*plist) + n 607 } 608 if index < 0 || index >= len(*plist) { 609 natsim.ircSendf("Bad filter index %d for %s%d", index, name, len(*plist)) 610 } else { 611 *plist = append((*plist)[:index], (*plist)[index+1:]...) 612 natsim.ircSendf("Removed filter %s%d/%d", name, index+1, len(*plist)+1) 613 } 614 } else { 615 natsim.ircSendf("Unable to parse place %q", uplace) 616 } 617 618 case "unsubscribe": 619 if n, err := strconv.Atoi(cmd.arg); err == nil && n > 0 && n <= len(natsim.subs) { 620 if err = natsim.subs[n-1].Unsubscribe(); err != nil { 621 natsim.ircSendError("Unsubscribe", err) 622 } else { 623 natsim.ircSendf("Unsubscribed from %q", natsim.subs[n-1].Subject) 624 natsim.subs = append(natsim.subs[:n-1], natsim.subs[n:]...) 625 } 626 } else { 627 n := 0 628 for i, s := range natsim.subs { 629 if s.Subject != cmd.arg { 630 natsim.subs[n] = natsim.subs[i] 631 n++ 632 } else if err = s.Unsubscribe(); err != nil { 633 natsim.ircSendError("Unsubscribe", err) 634 } 635 } 636 natsim.ircSendf("Unsubscribed from %d subjects", len(natsim.subs)-n) 637 natsim.subs = natsim.subs[:n] 638 } 639 640 case "version": 641 natsim.ircSendf("natsim %s", version) 642 643 default: 644 natsim.ircSendf("Unknown command %q", cmd.name) 645 } 646 } 647 } 648 649 /**************** IRC Callbacks ****************/ 650 651 func (natsim *NatsIM) ircJoin(e *irc.Event) { 652 natsim.irc.Join(natsim.Irc.Channel) 653 } 654 655 func (natsim *NatsIM) ircJoined(e *irc.Event) { 656 optSeed, err := nats.NkeyOptionFromSeed(natsim.Nats.NkeySeed) 657 if err != nil { 658 natsim.ircSendError("NkeyOptionFromSeed", err) 659 return 660 } 661 662 natsim.nc, err = nats.Connect(natsim.Nats.Server, 663 optSeed, 664 nats.Name(natsim.Nats.Name), 665 nats.RetryOnFailedConnect(natsim.Nats.RetryOnFailedConnect), 666 nats.ConnectHandler(natsim.natsConnected), 667 nats.DisconnectErrHandler(natsim.natsDisconnected), 668 nats.ReconnectHandler(natsim.natsReconnected), 669 nats.ReconnectErrHandler(natsim.natsReconnectErr)) 670 if err != nil { 671 natsim.ircSendError("Connect", err) 672 return 673 } 674 675 for _, subject := range natsim.Nats.Subjects { 676 if s, err := natsim.nc.Subscribe(subject, natsim.natsReceive); err != nil { 677 natsim.ircSendError("Subscribe", err) 678 } else { 679 natsim.subs = append(natsim.subs, s) 680 } 681 } 682 } 683 684 func (natsim *NatsIM) ircQuoteData(data []byte) string { 685 strdata := string(data) 686 suffix := 0 687 for len(strdata) > 0 && strdata[len(strdata)-1] == '\n' { 688 suffix++ 689 strdata = strdata[0 : len(strdata)-1] 690 } 691 692 var quoted strings.Builder 693 for i, line := range strings.Split(strdata, "\n") { 694 if i > 0 { 695 quoted.WriteString("\n") 696 } 697 qline := strconv.QuoteToGraphic(line) 698 if qline[0] != '"' || qline[len(qline)-1] != '"' { 699 panic("Expected double-quotes") 700 } 701 quoted.WriteString(qline[1 : len(qline)-1]) 702 } 703 quoted.WriteString(strings.Repeat("\\n", suffix)) 704 705 if natsim.Irc.MaxQuoteRatio < 0 || quoted.Len() < int(natsim.Irc.MaxQuoteRatio*float32(len(data))) { 706 s := quoted.String() 707 switch s[0] { 708 case '"', '#', '|', '<': 709 return "\"" + s + "\"" 710 default: 711 return s 712 } 713 } else if natsim.Irc.MaxHex < 0 || 2*len(data) <= natsim.Irc.MaxHex { 714 return "#" + hex.EncodeToString(data) + "#" 715 } else if natsim.Irc.MaxBase64 < 0 || base64.StdEncoding.EncodedLen(len(data)) <= natsim.Irc.MaxBase64 { 716 return "|" + base64.StdEncoding.EncodeToString(data) + "|" 717 } else { 718 return fmt.Sprintf("<%d-byte message>", len(data)) 719 } 720 } 721 722 func (natsim *NatsIM) ircReceive(e *irc.Event) { 723 msg := e.Message() 724 if name, arg, found := unpackMark(natsim.Irc.Cmd, msg, true); found { 725 if nickAllowed(e.Nick, natsim.Irc.AllowSend, natsim.Irc.BlockSend) { 726 natsim.cmdQueue <- command{name: name, arg: arg} 727 } 728 } else if subject, data, found := unpackMark(natsim.Irc.Send, msg, false); found { 729 if nickAllowed(e.Nick, natsim.Irc.AllowCmd, natsim.Irc.BlockCmd) { 730 if len(data) >= 2 && data[0] == data[len(data)-1] && (data[0] == '"' || data[0] == '`' || data[0] == '#' || data[0] == '|') { 731 switch data[0] { 732 case '#': 733 if decoded, err := hexDecode(data[1 : len(data)-1]); err != nil { 734 natsim.ircSendError("hexDecode", err) 735 return 736 } else { 737 natsim.curMsg.Data = decoded 738 } 739 case '|': 740 if decoded, err := b64Decode(data[1 : len(data)-1]); err != nil { 741 natsim.ircSendError("b64Decode", err) 742 return 743 } else { 744 natsim.curMsg.Data = decoded 745 } 746 default: 747 if unquoted, err := strconv.Unquote(data); err != nil { 748 natsim.ircSendError("Unquote", err) 749 return 750 } else { 751 natsim.curMsg.Data = []byte(unquoted) 752 } 753 } 754 } else if unquoted, err := strconv.Unquote("\"" + data + "\""); err == nil { 755 natsim.curMsg.Data = []byte(unquoted) 756 } else { 757 natsim.curMsg.Data = []byte(data) 758 } 759 760 natsim.curMsg.Subject = subject 761 762 if err := natsim.nc.PublishMsg(&natsim.curMsg); err != nil { 763 natsim.ircSendError("Publish", err) 764 } else { 765 natsim.logSent(&natsim.curMsg) 766 } 767 if natsim.Irc.AutoClear { 768 natsim.curMsg = nats.Msg{} 769 } 770 natsim.Irc.AutoClear = natsim.Irc.nextClear 771 } 772 } 773 } 774 775 func (natsim *NatsIM) ircSendError(context string, err error) { 776 prefix := "[E] " 777 if context != "" { 778 prefix += context + ": " 779 } 780 natsim.ircSend(prefix + err.Error()) 781 } 782 783 func (natsim *NatsIM) ircSend(s string) { 784 select { 785 case natsim.ircQueue <- s: 786 default: 787 natsim.dropped.Add(1) 788 } 789 } 790 791 func (natsim *NatsIM) ircSendf(format string, a ...interface{}) { 792 natsim.ircSend(fmt.Sprintf(format, a...)) 793 } 794 795 func (natsim *NatsIM) ircSender() { 796 var dropped uint32 797 var nindex = 0 798 floodend := make([]time.Time, natsim.Irc.AntiFlood.count) 799 delay := natsim.Irc.AntiFlood.delay / time.Duration(natsim.Irc.AntiFlood.count) 800 prev := time.Now() 801 802 for { 803 var lines []string 804 805 if len(natsim.ircQueue) == 0 { 806 dropped += natsim.dropped.Swap(0) 807 } 808 809 if dropped > 0 { 810 select { 811 case s, ok := <-natsim.ircQueue: 812 if ok { 813 lines = natsim.ircSplit(s) 814 } else { 815 return 816 } 817 case <-time.After(delay): 818 dropped += natsim.dropped.Swap(0) 819 lines = []string{fmt.Sprintf("Dropped %d messages", dropped)} 820 dropped = 0 821 } 822 } else { 823 s, ok := <-natsim.ircQueue 824 if ok { 825 lines = natsim.ircSplit(s) 826 } else { 827 return 828 } 829 } 830 831 for _, line := range lines { 832 if time.Until(floodend[nindex]) > 0 { 833 time.Sleep(time.Until(prev.Add(delay))) 834 } 835 836 natsim.irc.Privmsg(natsim.Irc.Channel, line) 837 838 prev = time.Now() 839 floodend[nindex] = prev.Add(natsim.Irc.AntiFlood.delay) 840 nindex = (nindex + 1) % natsim.Irc.AntiFlood.count 841 } 842 } 843 } 844 845 func (natsim *NatsIM) ircSplit(s string) []string { 846 var result []string 847 848 for _, line := range strings.Split(s, "\n") { 849 if natsim.Irc.MaxLine <= 0 || len(line) < natsim.Irc.MaxLine { 850 result = append(result, line) 851 } else { 852 for offset := 0; offset < len(line); { 853 var buf strings.Builder 854 l := len(line) - offset 855 if offset > 0 { 856 buf.WriteString(natsim.Irc.ContPrefix) 857 } 858 859 if buf.Len()+l <= natsim.Irc.MaxLine { 860 buf.WriteString(line[offset:]) 861 } else { 862 l = natsim.Irc.MaxLine - buf.Len() - len(natsim.Irc.ContSuffix) 863 buf.WriteString(line[offset : offset+l]) 864 buf.WriteString(natsim.Irc.ContSuffix) 865 } 866 867 result = append(result, buf.String()) 868 offset += l 869 } 870 } 871 } 872 873 return result 874 } 875 876 /**************** Nats Callbacks ****************/ 877 878 func (natsim *NatsIM) natsConnected(c *nats.Conn) { 879 natsim.ircSend("Connected to " + c.ConnectedUrlRedacted()) 880 } 881 882 func (natsim *NatsIM) natsDisconnected(c *nats.Conn, err error) { 883 if err != nil { 884 natsim.ircSendError("Disconnected", err) 885 } 886 } 887 888 func (natsim *NatsIM) natsReceive(m *nats.Msg) { 889 if !IsKept(m.Subject, m.Data, natsim.Nats.Filter, true) { 890 return 891 } 892 893 if IsKept(m.Subject, m.Data, natsim.Log.Filter, true) { 894 natsim.logReceived(m) 895 } 896 897 if !IsKept(m.Subject, m.Data, natsim.Irc.Filter, true) { 898 return 899 } 900 901 var sb strings.Builder 902 sb.WriteString(packMark(natsim.Irc.Show, m.Subject, natsim.ircQuoteData(m.Data))) 903 904 if m.Reply != "" && natsim.Irc.ShowReply != nil { 905 sb.WriteString(natsim.Irc.ShowReply.Start) 906 sb.WriteString(m.Reply) 907 sb.WriteString(natsim.Irc.ShowReply.End) 908 } 909 910 if natsim.Irc.ShowHeader != nil { 911 for key, values := range m.Header { 912 for _, value := range values { 913 sb.WriteString(packMark(*natsim.Irc.ShowHeader, key, value)) 914 } 915 } 916 } 917 918 natsim.ircSend(sb.String()) 919 } 920 921 func (natsim *NatsIM) natsReconnected(c *nats.Conn) { 922 natsim.ircSend("Reconnected to " + c.ConnectedUrlRedacted()) 923 } 924 925 func (natsim *NatsIM) natsReconnectErr(c *nats.Conn, err error) { 926 natsim.ircSendError("Reconnect", err) 927 } 928 929 func (natsim *NatsIM) natsStats() string { 930 stats := natsim.nc.Stats() 931 return fmt.Sprintf("%d reconnections, %s / %s msg in, %s / %s msg out", 932 stats.Reconnects, 933 humanize.IBytes(stats.InBytes), 934 humanizeNum(stats.InMsgs), 935 humanize.IBytes(stats.OutBytes), 936 humanizeNum(stats.OutMsgs)) 937 } 938 939 /**************** Log to Database ****************/ 940 941 //go:embed init.sql 942 var embeddedSQL embed.FS 943 944 func (natsim *NatsIM) logInit() error { 945 if natsim.Log.SqlDriver == "" { 946 return nil 947 } 948 949 var err error 950 951 natsim.db, err = sql.Open(natsim.Log.SqlDriver, natsim.Log.SqlConnection) 952 if err != nil { 953 log.Println("sql.Open:", err) 954 return err 955 } 956 957 var version int 958 if err = natsim.db.QueryRow("PRAGMA user_version").Scan(&version); err != nil { 959 log.Println("query user_verison", err) 960 return err 961 } 962 963 switch version { 964 case 0: 965 initSQL, err := embeddedSQL.ReadFile("init.sql") 966 if err != nil { 967 log.Println("embedded.ReadFile:", err) 968 return err 969 } 970 971 if _, err = natsim.db.Exec(string(initSQL)); err != nil { 972 log.Println("Init log DB:", err) 973 return err 974 } 975 976 case 1: 977 978 default: 979 log.Println("Unsupported database version:", version) 980 return errors.New("unsupported database version") 981 } 982 983 natsim.ensureSubject, err = natsim.db.Prepare("INSERT INTO subjects(name) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM subjects WHERE name = ?);") 984 if err != nil { 985 log.Println("Prepare ensureSubject:", err) 986 return err 987 } 988 989 natsim.insertReceived, err = natsim.db.Prepare("INSERT INTO received(timestamp,subject_id,reply_subject_id,data) VALUES (?, (SELECT id FROM subjects WHERE name = ?), (SELECT id FROM subjects WHERE name = ?), ?);") 990 if err != nil { 991 log.Println("Prepare insertReceived:", err) 992 return err 993 } 994 995 natsim.insertRHeader, err = natsim.db.Prepare("INSERT INTO received_headers_view(msg_id,key,value) VALUES (?,?,?);") 996 if err != nil { 997 log.Println("Prepare insertRHeader:", err) 998 return err 999 } 1000 1001 natsim.insertSent, err = natsim.db.Prepare("INSERT INTO sent(timestamp,subject_id,reply_subject_id,data) VALUES (?, (SELECT id FROM subjects WHERE name = ?), (SELECT id FROM subjects WHERE name = ?), ?);") 1002 if err != nil { 1003 log.Println("Prepare insertSent:", err) 1004 return err 1005 } 1006 1007 natsim.insertSHeader, err = natsim.db.Prepare("INSERT INTO sent_headers_view(msg_id,key,value) VALUES (?,?,?);") 1008 if err != nil { 1009 log.Println("Prepare insertSHeader:", err) 1010 return err 1011 } 1012 1013 return nil 1014 } 1015 1016 func (natsim *NatsIM) logMsg(msg *nats.Msg, insertMsg, insertHeader *sql.Stmt) { 1017 if natsim.db == nil || natsim.insertReceived == nil { 1018 return 1019 } 1020 1021 if _, err := natsim.ensureSubject.Exec(msg.Subject, msg.Subject); err != nil { 1022 natsim.ircSendError("ensureSubject.Exec", err) 1023 return 1024 } 1025 1026 var reply sql.NullString 1027 if msg.Reply != "" { 1028 if _, err := natsim.ensureSubject.Exec(msg.Reply, msg.Reply); err != nil { 1029 natsim.ircSendError("ensureReply.Exec", err) 1030 return 1031 } 1032 reply = sql.NullString{String: msg.Reply, Valid: true} 1033 } 1034 1035 t := float64(time.Now().UnixNano())/8.64e13 + 2440587.5 1036 if r, err := insertMsg.Exec(t, msg.Subject, reply, msg.Data); err != nil { 1037 natsim.ircSendError("insertMsg.Exec", err) 1038 } else if id, err := r.LastInsertId(); err != nil { 1039 natsim.ircSendError("LastInsertId", err) 1040 } else if id <= 0 { 1041 natsim.ircSendf("LastInsertId returned invalid id %d", id) 1042 } else { 1043 for key, values := range msg.Header { 1044 for _, value := range values { 1045 if _, err := insertHeader.Exec(id, key, value); err != nil { 1046 natsim.ircSendf("insertHeader(%q, %q): %s", key, value, err) 1047 } 1048 } 1049 } 1050 } 1051 } 1052 1053 func (natsim *NatsIM) logReceived(msg *nats.Msg) { 1054 natsim.logMsg(msg, natsim.insertReceived, natsim.insertRHeader) 1055 } 1056 1057 func (natsim *NatsIM) logSent(msg *nats.Msg) { 1058 natsim.logMsg(msg, natsim.insertSent, natsim.insertSHeader) 1059 } 1060 1061 /**************** Message Filters ****************/ 1062 1063 type FilterElement struct { 1064 Result bool 1065 Part FilterPart 1066 Test *regexp.Regexp 1067 } 1068 1069 type FilterPart int 1070 1071 const ( 1072 FilterSubject FilterPart = iota 1073 FilterData 1074 ) 1075 1076 func (element *FilterElement) Match(subject string, data []byte) bool { 1077 var b []byte 1078 switch element.Part { 1079 case FilterSubject: 1080 b = []byte(subject) 1081 case FilterData: 1082 b = data 1083 default: 1084 panic("Unexpected part") 1085 } 1086 1087 return element.Test.Match(b) 1088 } 1089 1090 func (element *FilterElement) String() string { 1091 r := "drop " 1092 if element.Result { 1093 r = "pass " 1094 } 1095 1096 p := "" 1097 switch element.Part { 1098 case FilterSubject: 1099 p = "subject " 1100 case FilterData: 1101 p = "data " 1102 default: 1103 panic("Unexpected part") 1104 } 1105 1106 return r + p + element.Test.String() 1107 } 1108 1109 func (element *FilterElement) UnmarshalText(text []byte) error { 1110 s := string(text) 1111 1112 switch { 1113 case strings.HasPrefix(s, "pass "): 1114 element.Result = true 1115 s = s[5:] 1116 case strings.HasPrefix(s, "drop "): 1117 element.Result = false 1118 s = s[5:] 1119 default: 1120 return fmt.Errorf("malformed filter %q", s) 1121 } 1122 1123 switch { 1124 case strings.HasPrefix(s, "subject "): 1125 element.Part = FilterSubject 1126 s = s[8:] 1127 case strings.HasPrefix(s, "data "): 1128 element.Part = FilterData 1129 s = s[5:] 1130 default: 1131 return fmt.Errorf("bad filter part %q", s) 1132 } 1133 1134 re, err := regexp.Compile(s) 1135 element.Test = re 1136 return err 1137 } 1138 1139 func WriteFilter(buf *strings.Builder, prefix string, filter []FilterElement) { 1140 for i, element := range filter { 1141 line := fmt.Sprintf("%s%d. %s", prefix, i+1, element.String()) 1142 buf.WriteString(line) 1143 } 1144 } 1145 1146 func IsKept(subject string, data []byte, elements []FilterElement, base bool) bool { 1147 for _, element := range elements { 1148 if element.Match(subject, data) { 1149 return element.Result 1150 } 1151 } 1152 1153 return base 1154 } 1155 1156 /**************** Nick Filters ****************/ 1157 1158 func updateNickList(nick string, directList, oppositeList *[]string) { 1159 if nick == "" { 1160 return 1161 } 1162 1163 n := 0 1164 for _, v := range *oppositeList { 1165 if v != nick { 1166 (*oppositeList)[n] = v 1167 n++ 1168 } 1169 } 1170 1171 if n < len(*oppositeList) { 1172 *oppositeList = (*oppositeList)[0:n] 1173 return 1174 } 1175 1176 for _, v := range *directList { 1177 if v == nick { 1178 return 1179 } 1180 } 1181 1182 *directList = append(*directList, nick) 1183 } 1184 1185 func nickAllowed(nick string, allowedList, blockedList []string) bool { 1186 for _, blocked := range blockedList { 1187 if nick == blocked { 1188 return false 1189 } 1190 } 1191 1192 for _, allowed := range allowedList { 1193 if nick == allowed { 1194 return true 1195 } 1196 } 1197 1198 return len(allowedList) == 0 1199 } 1200 1201 func strNickList(nickList []string) string { 1202 if len(nickList) == 0 { 1203 return "[]" 1204 } 1205 1206 var sb strings.Builder 1207 1208 for i, nick := range nickList { 1209 if i == 0 { 1210 sb.WriteString("[ ") 1211 } else { 1212 sb.WriteString(", ") 1213 } 1214 1215 sb.WriteString(fmt.Sprintf("%q", nick)) 1216 } 1217 1218 sb.WriteString(" ]") 1219 return sb.String() 1220 } 1221 1222 /**************** Tools ****************/ 1223 1224 type antiflood struct { 1225 count int 1226 delay time.Duration 1227 } 1228 1229 func (af *antiflood) UnmarshalText(text []byte) error { 1230 if before, after, found := strings.Cut(string(text), "/"); found { 1231 if n, err := strconv.Atoi(before); err != nil { 1232 return err 1233 } else { 1234 af.count = n 1235 } 1236 1237 if d, err := time.ParseDuration(after); err != nil { 1238 return err 1239 } else { 1240 af.delay = d 1241 } 1242 1243 } else if d, err := time.ParseDuration(string(text)); err != nil { 1244 return err 1245 } else { 1246 af.count = 1 1247 af.delay = d 1248 } 1249 1250 return nil 1251 } 1252 1253 func b64Decode(s string) ([]byte, error) { 1254 stripped := strings.ReplaceAll(s, " ", "") 1255 return base64.StdEncoding.DecodeString(stripped) 1256 } 1257 1258 func hexDecode(s string) ([]byte, error) { 1259 stripped := strings.ReplaceAll(s, " ", "") 1260 return hex.DecodeString(stripped) 1261 } 1262 1263 func humanizeNum(n uint64) string { 1264 num, unit, found := strings.Cut(humanize.Bytes(n), " ") 1265 if !found || unit == "" || unit[len(unit)-1:] != "B" { 1266 panic("Unexpected huamized result") 1267 } 1268 return num + unit[:len(unit)-1] 1269 } 1270 1271 func packMark(mark LineMark, name, arg string) string { 1272 return mark.Start + name + mark.Mid + arg + mark.End 1273 } 1274 1275 func unpackMark(mark LineMark, line string, optional bool) (string, string, bool) { 1276 if strings.HasPrefix(line, mark.Start) && strings.HasSuffix(line, mark.End) { 1277 inside := line[len(mark.Start) : len(line)-len(mark.End)] 1278 if mark.Mid == "" { 1279 return inside, "", true 1280 } else if name, arg, found := strings.Cut(inside, mark.Mid); found { 1281 return name, arg, true 1282 } else { 1283 return inside, "", optional 1284 } 1285 } else { 1286 return "", "", false 1287 } 1288 } 1289 1290 var version = "(unknown)" 1291 1292 func setVersion() { 1293 info, ok := debug.ReadBuildInfo() 1294 if !ok { 1295 return 1296 } 1297 1298 version = info.Main.Version 1299 1300 if version == "(devel)" { 1301 vcs := "" 1302 rev := "" 1303 dirty := "" 1304 for _, setting := range info.Settings { 1305 switch setting.Key { 1306 case "vcs": 1307 vcs = setting.Value + "-" 1308 case "vcs.revision": 1309 rev = setting.Value[0:8] 1310 case "vcs.modified": 1311 if setting.Value == "true" { 1312 dirty = "*" 1313 } 1314 } 1315 } 1316 1317 if rev != "" { 1318 version = vcs + rev + dirty 1319 } 1320 } 1321 }