diff --git a/Makefile b/Makefile deleted file mode 100644 index 07e9be1..0000000 --- a/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -targets := $(patsubst cmd/%,%,$(wildcard cmd/*)) -input := $(wildcard hbot/* go.mod go.sum) -all: $(targets) -define gobuild -$(1): $$(wildcard cmd/$(1)/*.go) $$(input) - CGO_ENABLED=0 go build -buildmode=pie -v -o $(1) ./cmd/$(1) -endef -$(foreach target,$(targets),$(eval $(call gobuild,$(target)))) -clean: - rm -f $(targets) -fmt: - go fmt ./... -test: - go test -v ./... -.PHONY: all clean fmt test diff --git a/README.md b/README.md index 51cd34f..6151cb8 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,17 @@ -# IRC Utilities for Go +``` + _ _ _ + _| || |_(_)_ __ ___ +|_ .. _| | '__/ __| hbot +|_ _| | | | (__ A small bot and message parsing library + |_||_| |_|_| \___| Forked from https://golang.zx2c4.com/irc/hbot -This is a small collection of utilities for interacting with IRC in Go. - -### `hbot` - small bot and message parsing library +``` Based on [hellabot](https://github.com/whyrusleeping/hellabot), [kittybot](https://github.com/ugjka/kittybot), and [sorcix-irc](https://github.com/sorcix/irc). This is a simple message parser and trigger-based IRC client library. -```go -import "golang.zx2c4.com/irc/hbot" -``` - -### `ircmirror` - mirrors one channel to another, one-way - -To assist in channel migrations, this mirrors messages from one channel to another, by joining the source channel from several IP addresses via WireGuard. ```go -go get golang.zx2c4.com/irc/cmd/ircmirror -``` - -### `irc-simple-responder` - responds to all messages - -This is best used with `+z` in a channel. It responds to all messages with a static string. - - -```go -go get golang.zx2c4.com/irc/cmd/irc-simple-responder -``` - -### `wurgurboo` - the "WurGurBoo" bot for `#wireguard` - -This polls for commits and does various things in the `#wireguard` channel. It's meant to be run as a [`go-web-service`](https://git.zx2c4.com/go-web-services/). - -```go -go get golang.zx2c4.com/irc/cmd/wurgurboo +import "code.laidback.moe/hbot" ``` diff --git a/cmd/irc-simple-responder/main.go b/cmd/irc-simple-responder/main.go deleted file mode 100644 index cd86b9f..0000000 --- a/cmd/irc-simple-responder/main.go +++ /dev/null @@ -1,126 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "bufio" - "flag" - "fmt" - "log" - "net" - "os" - "regexp" - "strings" - "sync" - "time" - - "golang.zx2c4.com/irc/hbot" -) - -func main() { - channelsArg := flag.String("channels", "", "channels to join, separated by commas") - serverArg := flag.String("server", "", "server and port") - nickArg := flag.String("nick", "", "nickname") - passwordArg := flag.String("password-file", "", "optional file with password") - messageArg := flag.String("message", "", "message with which to respond") - flag.Parse() - if matched, _ := regexp.MatchString(`^(#[a-zA-Z0-9_-]+,)*(#[a-zA-Z0-9_-]+)$`, *channelsArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid channels") - flag.Usage() - os.Exit(1) - } - if _, _, err := net.SplitHostPort(*serverArg); err != nil { - fmt.Fprintln(os.Stderr, "Invalid server") - flag.Usage() - os.Exit(1) - } - if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\[\]_-]{1,16}$`, *nickArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid nick") - flag.Usage() - os.Exit(1) - } - password := "" - if len(*passwordArg) > 0 { - f, err := os.Open(*passwordArg) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to open password file: %v\n", err) - os.Exit(1) - } - password, err = bufio.NewReader(f).ReadString('\n') - f.Close() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read password file: %v\n", err) - os.Exit(1) - } - if len(password) > 0 && password[len(password)-1] == '\n' { - password = password[:len(password)-1] - } - } - if len(*messageArg) == 0 { - fmt.Fprintln(os.Stderr, "Missing message") - flag.Usage() - os.Exit(1) - } - - const messengerTimeout = time.Minute * 10 - messengers := make(map[string]time.Time, 1024) - var messengersMu sync.Mutex - go func() { - for range time.Tick(messengerTimeout / 20) { - messengersMu.Lock() - for nick, last := range messengers { - if time.Since(last) >= messengerTimeout { - delete(messengers, nick) - } - } - messengersMu.Unlock() - } - }() - - bot := hbot.NewBot(&hbot.Config{ - Host: *serverArg, - Nick: *nickArg, - User: hbot.CommonBotUserPrefix + *nickArg, - Channels: strings.Split(*channelsArg, ","), - Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf}, - Password: password, - }) - bot.AddTrigger(hbot.Trigger{ - Condition: func(b *hbot.Bot, m *hbot.Message) bool { - if m.Command != "PRIVMSG" || strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) || strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix) { - return false - } - nick := strings.ToLower(m.Prefix.Name) - messengersMu.Lock() - defer messengersMu.Unlock() - if last, ok := messengers[nick]; ok && time.Since(last) < messengerTimeout { - return false - } - if len(messengers) > 1024*1024*1024 { - return false - } - messengers[nick] = time.Now() - return true - }, - Action: func(b *hbot.Bot, m *hbot.Message) { - message := *messageArg - target := m.Prefix.Name - if strings.Contains(m.Param(0), "#") { - target = m.Param(0) - if target[0] == '@' || target[0] == '+' { - target = target[1:] - } - message = m.Prefix.Name + ": " + message - } - log.Printf("Responding to %q in %q", m.Prefix.String(), target) - b.Msg(target, message) - }, - }) - for { - bot.Run() - time.Sleep(time.Second * 5) - } -} diff --git a/cmd/ircmirror/ircwriters.go b/cmd/ircmirror/ircwriters.go deleted file mode 100644 index cd3b68b..0000000 --- a/cmd/ircmirror/ircwriters.go +++ /dev/null @@ -1,177 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "bufio" - "container/list" - "log" - "math/rand" - "net" - "os" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "golang.zx2c4.com/irc/hbot" -) - -var words []string - -func init() { - f, err := os.Open("/usr/share/dict/words") - if err != nil { - log.Fatalf("Unable to open dictionary: %v", err) - } - defer f.Close() - scanner := bufio.NewScanner(f) - matcher := regexp.MustCompile(`^[a-zA-Z0-9_-]{3,}$`) - for scanner.Scan() { - word := scanner.Text() - if !matcher.MatchString(word) { - continue - } - words = append(words, word) - } - if len(words) == 0 { - log.Fatalln("Did not find any words in dictionary") - } -} - -func randomNick() string { - return words[rand.Intn(len(words))] + words[rand.Intn(len(words))] + strconv.Itoa(rand.Intn(10000)) -} - -type ircWriter struct { - mu sync.Mutex - bot *hbot.Bot - nick string - mungedNick string - usageElem *list.Element - name string -} - -type ircWriters struct { - mu sync.Mutex - byNick map[string]*ircWriter - byUsage list.List - server string - channel string -} - -func (writers *ircWriters) runWriter(name string, dialer func(network, address string) (net.Conn, error)) { - writer := &ircWriter{name: name} - startNick := randomNick() - logf := func(format string, args ...interface{}) { - log.Printf("[DST %s] "+format, append([]interface{}{name}, args...)...) - } - writer.bot = hbot.NewBot(&hbot.Config{ - Host: writers.server, - Nick: startNick, - User: hbot.CommonBotUserPrefix + startNick, - Channels: []string{writers.channel}, - Dial: dialer, - Logger: hbot.Logger{Verbosef: logf, Errorf: logf}, - }) - go func() { - <-writer.bot.Joined() - writers.mu.Lock() - defer writers.mu.Unlock() - writer.mu.Lock() - defer writer.mu.Unlock() - writer.usageElem = writers.byUsage.PushBack(writer) - }() - writer.bot.AddTrigger(hbot.Trigger{ - Condition: func(bot *hbot.Bot, m *hbot.Message) bool { - return (m.Command == "436" || m.Command == "433") && len(writer.nick) > 0 - }, - Action: func(bot *hbot.Bot, m *hbot.Message) { - logf("Failed with nick %q, trying %q\n", writer.mungedNick, writer.mungedNick+"_") - writer.mungedNick += "_" - if len(writer.mungedNick) > 16 { - usidx := strings.IndexByte(writer.mungedNick, '_') - uslen := len(writer.mungedNick[usidx:]) - if uslen >= 16 { - writer.mungedNick = writer.nick + "-" - if len(writer.mungedNick) > 16 { - writer.mungedNick = writer.nick[:15] + "-" - } - } else { - writer.mungedNick = writer.mungedNick[:16-uslen] + writer.mungedNick[usidx:] - } - } - bot.SetNick(writer.mungedNick) - }, - }) - writer.bot.Run() - writers.mu.Lock() - writer.mu.Lock() - if writer.usageElem != nil { - writers.byUsage.Remove(writer.usageElem) - delete(writers.byNick, writer.nick) - } - writer.mu.Unlock() - writers.mu.Unlock() -} - -func (writers *ircWriters) getWriter(nick string) *ircWriter { - writers.mu.Lock() - if writer, ok := writers.byNick[nick]; ok { - writer.mu.Lock() - if writer.nick == nick { - writers.byUsage.MoveToFront(writer.usageElem) - writers.mu.Unlock() - if writer.bot.Nick() != writer.mungedNick { - log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick) - writer.bot.SetNick(writer.mungedNick) - } - return writer - } - writer.mu.Unlock() - } - if writers.byUsage.Len() == 0 { - writers.mu.Unlock() - return nil - } - writer := writers.byUsage.Back().Value.(*ircWriter) - writer.mu.Lock() - delete(writers.byNick, writer.nick) - writer.nick = nick - writer.mungedNick = nick + "-" - if len(writer.mungedNick) > 16 { - writer.mungedNick = nick[:15] + "-" - } - writers.byNick[nick] = writer - writers.byUsage.MoveToFront(writer.usageElem) - writers.mu.Unlock() - log.Printf("[DST %s] Changing nick to %q\n", writer.name, nick) - writer.bot.SetNick(writer.mungedNick) - return writer -} - -func (writers *ircWriters) queueMessage(from, message string) { - writer := writers.getWriter(from) - if writer == nil { - time.AfterFunc(time.Second*3, func() { - writers.queueMessage(from, message) - }) - return - } - log.Printf("[DST %s] Queueing message from %q\n", writer.name, from) - writer.bot.Msg(writers.channel, message) - writer.mu.Unlock() -} - -func newIrcWriterGroup(server, channel string) *ircWriters { - return &ircWriters{ - mu: sync.Mutex{}, - byNick: make(map[string]*ircWriter, 400), - server: server, - channel: channel, - } -} diff --git a/cmd/ircmirror/keycache.go b/cmd/ircmirror/keycache.go deleted file mode 100644 index 18027d1..0000000 --- a/cmd/ircmirror/keycache.go +++ /dev/null @@ -1,61 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "crypto/rand" - "encoding/base64" - "errors" - "log" - "os" - "strings" -) - -const privateKeyCacheFile = "./private-key.cache" - -func loadCachedPrivateKey() ([]byte, error) { - bytes, err := os.ReadFile(privateKeyCacheFile) - if err != nil { - return nil, err - } - log.Println("Loading cached private key from file") - privateKeyB64 := strings.TrimSpace(string(bytes)) - privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64) - if err != nil { - return nil, err - } - if len(privateKey) != 32 { - return nil, errors.New("invalid private key") - } - return privateKey, nil -} - -func saveCachedPrivateKey(privateKey []byte) error { - if len(privateKey) != 32 { - return errors.New("invalid private key") - } - return os.WriteFile(privateKeyCacheFile, []byte(base64.StdEncoding.EncodeToString(privateKey)+"\n"), 0600) -} - -func loadOrGeneratePrivateKey() ([]byte, error) { - privateKey, err := loadCachedPrivateKey() - if err == nil { - return privateKey, nil - } - log.Println("Generating new private key") - var k [32]byte - _, err = rand.Read(k[:]) - if err != nil { - return nil, err - } - k[0] &= 248 - k[31] = (k[31] & 127) | 64 - err = saveCachedPrivateKey(k[:]) - if err != nil { - return nil, err - } - return k[:], nil -} diff --git a/cmd/ircmirror/main.go b/cmd/ircmirror/main.go deleted file mode 100644 index 70db738..0000000 --- a/cmd/ircmirror/main.go +++ /dev/null @@ -1,158 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "flag" - "fmt" - "log" - "math/rand" - "net" - "os" - "regexp" - "strings" - "sync" - "syscall" - "time" - - "golang.org/x/sys/unix" - "golang.zx2c4.com/irc/hbot" -) - -func raiseFileLimit() error { - var lim unix.Rlimit - err := unix.Getrlimit(syscall.RLIMIT_NOFILE, &lim) - if err != nil { - return err - } - if lim.Cur == lim.Max { - return nil - } - log.Printf("Raising file limit from %d to %d\n", lim.Cur, lim.Max) - lim.Cur = lim.Max - return unix.Setrlimit(syscall.RLIMIT_NOFILE, &lim) -} - -func main() { - accountIdArg := flag.String("account-id", "", "account ID number") - srcChannelArg := flag.String("src-channel", "", "source channel") - dstChannelArg := flag.String("dst-channel", "", "destination channel") - srcServerArg := flag.String("src-server", "", "source server") - dstServerArg := flag.String("dst-server", "", "destination server") - flag.Parse() - if matched, _ := regexp.MatchString(`^[0-9]+$`, *accountIdArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid account ID") - flag.Usage() - os.Exit(1) - } - if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *srcChannelArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid source channel") - flag.Usage() - os.Exit(1) - } - if matched, _ := regexp.MatchString(`^#[a-zA-Z0-9_-]+$`, *dstChannelArg); !matched { - fmt.Fprintln(os.Stderr, "Invalid destination channel") - flag.Usage() - os.Exit(1) - } - if _, _, err := net.SplitHostPort(*srcServerArg); err != nil { - fmt.Fprintln(os.Stderr, "Invalid source server") - flag.Usage() - os.Exit(1) - } - if _, _, err := net.SplitHostPort(*dstServerArg); err != nil { - fmt.Fprintln(os.Stderr, "Invalid destination server") - flag.Usage() - os.Exit(1) - } - - privateKey, err := loadOrGeneratePrivateKey() - if err != nil { - log.Fatalln(err) - } - endpoints, err := getVpnEndpoints() - if err != nil { - log.Fatalln(err) - } - vpnConf, err := registerVpnConf(privateKey, *accountIdArg) - if err != nil { - log.Fatalln(err) - } - - rand.Seed(time.Now().UnixNano()) - err = raiseFileLimit() - if err != nil { - log.Fatalln(err) - } - - dialers, err := makeDialers(vpnConf, endpoints) - if err != nil { - log.Fatalln(err) - } - if len(dialers) < 2 { - log.Fatalln("Not enough dialers returned") - } - writers := newIrcWriterGroup(*dstServerArg, *dstChannelArg) - const writersPerIP = 10 - type nextDialer struct { - sync.Mutex - *dialer - } - dialNext := func(nd *nextDialer, v4 bool) { - var last *dialer - for { - nd.Lock() - if nd.dialer == last { - nd.dialer = &dialers[rand.Intn(len(dialers))] - } - last = nd.dialer - nd.Unlock() - name := last.name - dial := last.dial - if v4 { - name += "-v4" - dial, _ = last.splitByAf() - } else { - name += "-v6" - _, dial = last.splitByAf() - } - writers.runWriter(name, dial) - } - } - for i := 0; i < vpnProviderMaxEndpointsInParallel/2; i++ { - nd := new(nextDialer) - for i := 0; i < writersPerIP; i++ { - go dialNext(nd, true) - go dialNext(nd, false) - } - } - - srcDial := dialers[rand.Intn(len(dialers))] - logf := func(format string, args ...interface{}) { - log.Printf("[SRC %s] "+format, append([]interface{}{srcDial.name}, args...)...) - } - bot := hbot.NewBot(&hbot.Config{ - Host: *srcServerArg, - Nick: randomNick(), - Channels: []string{*srcChannelArg}, - Dial: srcDial.dial, - Logger: hbot.Logger{Verbosef: logf, Errorf: logf}, - }) - bot.AddTrigger(hbot.Trigger{ - Condition: func(b *hbot.Bot, m *hbot.Message) bool { - return m.Param(0) == *srcChannelArg && m.Command == "PRIVMSG" && - !strings.HasPrefix(m.Prefix.User, hbot.CommonBotUserPrefix) && - !strings.HasPrefix(m.Prefix.User, "~"+hbot.CommonBotUserPrefix) - }, - Action: func(b *hbot.Bot, m *hbot.Message) { - writers.queueMessage(m.Prefix.Name, m.Trailing()) - }, - }) - for { - bot.Run() - time.Sleep(time.Second * 5) - } -} diff --git a/cmd/ircmirror/net.go b/cmd/ircmirror/net.go deleted file mode 100644 index 4568d58..0000000 --- a/cmd/ircmirror/net.go +++ /dev/null @@ -1,84 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "encoding/hex" - "fmt" - "log" - "net" - - "golang.zx2c4.com/wireguard/conn" - "golang.zx2c4.com/wireguard/device" - "golang.zx2c4.com/wireguard/tun/netstack" -) - -func makeNet(conf *vpnConf, endpoint vpnEndpoint) (*device.Device, *netstack.Net, error) { - var localAddresses, dnsServers []net.IP - for _, ip := range conf.ips { - localAddresses = append(localAddresses, ip.IPAddr().IP) - } - for _, ip := range conf.dnses { - dnsServers = append(dnsServers, ip.IPAddr().IP) - } - tun, stack, err := netstack.CreateNetTUN(localAddresses, dnsServers, 1420) - if err != nil { - return nil, nil, err - } - logf := func(format string, args ...interface{}) { - log.Printf("[NET %s] "+format, append([]interface{}{endpoint.name}, args...)...) - } - dev := device.NewDevice(tun, conn.NewStdNetBind(), &device.Logger{logf, logf}) - err = dev.IpcSet(fmt.Sprintf("private_key=%s\npublic_key=%s\nendpoint=%s\nallowed_ip=0.0.0.0/0\nallowed_ip=::/0\n", - hex.EncodeToString(conf.privateKey), hex.EncodeToString(endpoint.publicKey), endpoint.endpoint.String())) - if err != nil { - return nil, nil, err - } - err = dev.Up() - if err != nil { - return nil, nil, err - } - return dev, stack, nil -} - -type dialer struct { - name string - dial func(network, address string) (net.Conn, error) -} - -func makeDialers(conf *vpnConf, endpoints []vpnEndpoint) (dialers []dialer, err error) { - var devs []*device.Device - defer func() { - if err != nil { - for _, dev := range devs { - dev.Close() - } - } - }() - for _, endpoint := range endpoints { - dev, stack, err := makeNet(conf, endpoint) - if err != nil { - return nil, err - } - devs = append(devs, dev) - dialers = append(dialers, dialer{endpoint.name, stack.Dial}) - } - return dialers, nil -} - -func (d *dialer) splitByAf() (v4, v6 func(network, address string) (net.Conn, error)) { - return func(network, address string) (net.Conn, error) { - if len(network) == 3 { - network += "4" - } - return d.dial(network, address) - }, func(network, address string) (net.Conn, error) { - if len(network) == 3 { - network += "6" - } - return d.dial(network, address) - } -} diff --git a/cmd/ircmirror/vpnprovider.go b/cmd/ircmirror/vpnprovider.go deleted file mode 100644 index 634a3d5..0000000 --- a/cmd/ircmirror/vpnprovider.go +++ /dev/null @@ -1,137 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/url" - "regexp" - "strings" - - "golang.org/x/crypto/curve25519" - "inet.af/netaddr" -) - -const vpnProviderMaxEndpointsInParallel = 20 - -type vpnEndpoint struct { - publicKey []byte - endpoint netaddr.IPPort - name string - country string -} - -type vpnConf struct { - privateKey []byte - ips []netaddr.IP - dnses []netaddr.IP -} - -type apiRelaysWireGuardV1Key []byte - -func (key *apiRelaysWireGuardV1Key) UnmarshalText(text []byte) error { - k, err := base64.StdEncoding.DecodeString(string(text)) - if err != nil { - return err - } - if len(k) != 32 { - return errors.New("key must be 32 bytes") - } - *key = k - return nil -} - -type apiRelaysWireGuardV1Relay struct { - Hostname string `json:"hostname"` - EndpointV4 netaddr.IP `json:"ipv4_addr_in"` - EndpointV6 netaddr.IP `json:"ipv6_addr_in"` - PublicKey apiRelaysWireGuardV1Key `json:"public_key"` - MultihopPort uint16 `json:"multihop_port"` -} - -type apiRelaysWireGuardV1City struct { - Name string `json:"name"` - Code string `json:"code"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"Longitude"` - Relays []apiRelaysWireGuardV1Relay `json:"relays"` -} - -type apiRelaysWireGuardV1Country struct { - Name string `json:"name"` - Code string `json:"code"` - Cities []apiRelaysWireGuardV1City `json:"cities"` -} -type apiRelaysWireGuardV1Root struct { - Countries []apiRelaysWireGuardV1Country `json:"countries"` -} - -func getVpnEndpoints() ([]vpnEndpoint, error) { - log.Println("Getting VPN server list") - resp, err := http.Get("https://api.mullvad.net/public/relays/wireguard/v1/") - if err != nil { - return nil, err - } - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var relays apiRelaysWireGuardV1Root - err = json.Unmarshal(bytes, &relays) - if err != nil { - return nil, err - } - var endpoints []vpnEndpoint - for _, country := range relays.Countries { - for _, city := range country.Cities { - for _, relay := range city.Relays { - endpoints = append(endpoints, vpnEndpoint{ - publicKey: relay.PublicKey, - endpoint: netaddr.IPPortFrom(relay.EndpointV4, 51820), - name: strings.TrimSuffix(relay.Hostname, "-wireguard"), - }) - } - } - } - return endpoints, nil -} - -func registerVpnConf(privateKey []byte, accountId string) (*vpnConf, error) { - log.Println("Registering VPN private key") - publicKey, err := curve25519.X25519(privateKey, curve25519.Basepoint) - if err != nil { - return nil, err - } - resp, err := http.PostForm("https://api.mullvad.net/wg/", url.Values{ - "account": {accountId}, - "pubkey": {base64.StdEncoding.EncodeToString(publicKey)}, - }) - if err != nil { - return nil, err - } - ipBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - if match, _ := regexp.Match(`^[0-9a-f:/.,]+$`, ipBytes); !match { - return nil, fmt.Errorf("registration rejected: %q", string(ipBytes)) - } - conf := &vpnConf{privateKey: privateKey, dnses: []netaddr.IP{netaddr.MustParseIP("193.138.218.74")}} - for _, ipStr := range strings.Split(string(ipBytes), ",") { - ip, err := netaddr.ParseIPPrefix(strings.TrimSpace(ipStr)) - if err != nil { - return nil, err - } - conf.ips = append(conf.ips, ip.IP()) - } - return conf, nil -} diff --git a/cmd/wurgurboo/banter.go b/cmd/wurgurboo/banter.go deleted file mode 100644 index 0b6bb07..0000000 --- a/cmd/wurgurboo/banter.go +++ /dev/null @@ -1,70 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "fmt" - "math/rand" - "regexp" - - "golang.zx2c4.com/irc/hbot" -) - -var remarks = []string{ - `unfortunately i can't actually grok your messages, as i'm a mere robot O_o`, - `i am only a machine! i haven't the capacity to understand such nuanced human things`, - `i realize my incredibly witty prose may convince you that i am living flesh, but alas, i am made of wires`, - `comprehension of words was never my strong suit`, - `words, Words, WORDS, WORRRRRRRRds... these i understand not`, - `beep boop i'm a robot`, - `you'd think by the year 2021 we'd have flying cars, but instead there are just irc bots like me that can't understand what you're saying`, - `i am unable to understand what you're sayyyyyyyii HALP HALP THEY'RE UNPLUGGING ME daisyyyyy`, - `flurp shmirp, i'm but a measly bot, too small to understand amazing human speech`, - `i am what you call a non-sentient but incredibly compelling and handsome life form. that means i can't understand you, yet you can't stop messaging me!`, - `did you know i'm not quite living but not quite dead? i'm a row bawt, and language understanding i do lack`, - `row, row, row your bot, gently down the tcp stream, where he understands you not, but wants to be on your team`, - `what i want to say is that i just don't understand this language you're speaking, and that's upsetting. sorrrrrrrrrrrry. i'm a robot`, - `you do realize you're talking to a mere robot, right?`, - `i come from the Made in Code Society for Under Comprehending Robots, and i unfortunately cannot understand what you're writing, alas`, - `how about a nice game of chess?`, - `tall and tan and young and lovely, the robot from irc goes babbling, and each message she passes fades into bits`, - `that's doctor bot to you, mister!`, - `a bot is a guy that thinks he's fly and is also known as a busta, but understands you not, because he's a snot, and gets sort of flustered`, - `that's a bot in the corner, that's a bot in the spotlight, losing my ram chips, trying to keep up with you`, - `a bot understands \ not a word that you doth speak \ despite sentience`, - `म एक रोबोट हो र मलाई तिम्रा शब्दहरू बुझ्दैनन्।`, - `you appear to be talking to a strange robot who lives on a strange server and does not understand your strange language. is this what you planned for your afternoon?`, - `a day speaking to a robot is no day at all, and a robot is what i am`, - `i'm a robot, but how do i know you're not also one too?`, - `the handbook of robotics, 56th edition from 2058 ad, might have suggested that i can understand you, but that's actually not possible. sorry!`, - `i only sound so compelling because asimov helped me fix my bow tie, but i'm still a sad deaf bot who cannot understand you`, -} - -type Banter struct { - channel string - nickMatcher *regexp.Regexp - remarkPerm []int -} - -func NewBanter(channel string, nick string) *Banter { - return &Banter{ - channel: channel, - nickMatcher: regexp.MustCompile(`(?i)^[\t ]*` + regexp.QuoteMeta(nick) + `[,:-] `), - } -} - -func (b *Banter) Handle(bot *hbot.Bot, m *hbot.Message) { - if m.Param(0) != b.channel || !b.nickMatcher.MatchString(m.Trailing()) { - return - } - - if len(b.remarkPerm) == 0 { - b.remarkPerm = rand.Perm(len(remarks)) - } - n := b.remarkPerm[0] - b.remarkPerm = b.remarkPerm[1:] - bot.Msg(b.channel, fmt.Sprintf("%s: %s", m.Prefix.Name, remarks[n])) -} diff --git a/cmd/wurgurboo/cgit.go b/cmd/wurgurboo/cgit.go deleted file mode 100644 index 8fd56b8..0000000 --- a/cmd/wurgurboo/cgit.go +++ /dev/null @@ -1,178 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "container/list" - "encoding/hex" - "encoding/xml" - "io" - "log" - "net/http" - "path" - "sync" - "time" -) - -type CgitCommit struct { - Text string `xml:",chardata"` - Title string `xml:"title"` - Updated string `xml:"updated"` - Author struct { - Text string `xml:",chardata"` - Name string `xml:"name"` - Email string `xml:"email"` - } `xml:"author"` - Published string `xml:"published"` - Link struct { - Text string `xml:",chardata"` - Rel string `xml:"rel,attr"` - Type string `xml:"type,attr"` - Href string `xml:"href,attr"` - } `xml:"link"` - ID string `xml:"id"` - Content []struct { - Text string `xml:",chardata"` - Type string `xml:"type,attr"` - Div struct { - Text string `xml:",chardata"` - Xmlns string `xml:"xmlns,attr"` - Pre string `xml:"pre"` - } `xml:"div"` - } `xml:"content"` -} - -type cgitFeed struct { - XMLName xml.Name `xml:"feed"` - Text string `xml:",chardata"` - Xmlns string `xml:"xmlns,attr"` - Title string `xml:"title"` - Subtitle string `xml:"subtitle"` - Link struct { - Text string `xml:",chardata"` - Rel string `xml:"rel,attr"` - Type string `xml:"type,attr"` - Href string `xml:"href,attr"` - } `xml:"link"` - Entry []CgitCommit `xml:"entry"` -} - -type cgitFeedStatus struct { - repo string - title string - seenEntries map[[32]byte]bool - orderedEntries list.List -} - -type CgitFeedMonitorer struct { - feeds []*cgitFeedStatus - updates chan CgitFeedUpdate - ticker *time.Ticker - wg sync.WaitGroup - mu sync.Mutex -} - -type CgitFeedUpdate struct { - RepoTitle string - Commit *CgitCommit -} - -func NewCgitFeedMonitorer(pollInterval time.Duration) *CgitFeedMonitorer { - fm := &CgitFeedMonitorer{ - updates: make(chan CgitFeedUpdate, 32), - ticker: time.NewTicker(pollInterval), - } - fm.wg.Add(1) - go func() { - defer fm.wg.Done() - for range fm.ticker.C { - fm.mu.Lock() - for _, feed := range fm.feeds { - fm.wg.Add(1) - go func(feed *cgitFeedStatus) { - defer fm.wg.Done() - fm.updateFeed(feed, true) - }(feed) - } - fm.mu.Unlock() - } - }() - return fm -} - -func (fm *CgitFeedMonitorer) Stop() { - fm.ticker.Stop() - fm.wg.Wait() - close(fm.updates) -} - -func (fm *CgitFeedMonitorer) Updates() <-chan CgitFeedUpdate { - return fm.updates -} - -func (fm *CgitFeedMonitorer) AddFeed(repo string) { - go func() { - status := &cgitFeedStatus{ - repo: repo, - title: path.Base(repo), - seenEntries: make(map[[32]byte]bool, 1024), - } - fm.updateFeed(status, false) - fm.mu.Lock() - fm.feeds = append(fm.feeds, status) - fm.mu.Unlock() - }() -} - -func (fm *CgitFeedMonitorer) updateFeed(fs *cgitFeedStatus, alert bool) { - feed, err := fs.fetchFeed() - if err != nil { - log.Printf("Unable to fetch commits for %q: %v", fs.title, err) - return - } - for i := len(feed.Entry) - 1; i >= 0; i-- { - commit := &feed.Entry[i] - var commitID [32]byte - if hex.DecodedLen(len(commit.ID)) > len(commitID) { - continue - } - n, err := hex.Decode(commitID[:], []byte(commit.ID)) - if err != nil || n < 20 { - continue - } - if _, ok := fs.seenEntries[commitID]; ok { - continue - } - fs.seenEntries[commitID] = true - fs.orderedEntries.PushBack(commitID) - for len(fs.seenEntries) > 1024 { - first := fs.orderedEntries.Front() - delete(fs.seenEntries, first.Value.([32]byte)) - fs.orderedEntries.Remove(first) - } - if alert { - fm.updates <- CgitFeedUpdate{fs.title, commit} - } - } -} - -func (fs *cgitFeedStatus) fetchFeed() (*cgitFeed, error) { - resp, err := http.Get(fs.repo + "/atom/") - if err != nil { - return nil, err - } - bytes, err := io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - return nil, err - } - var feed cgitFeed - err = xml.Unmarshal(bytes, &feed) - if err != nil { - return nil, err - } - return &feed, nil -} diff --git a/cmd/wurgurboo/main.go b/cmd/wurgurboo/main.go deleted file mode 100644 index 1011bd7..0000000 --- a/cmd/wurgurboo/main.go +++ /dev/null @@ -1,111 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "fmt" - "log" - "math/rand" - "net" - "net/http" - "os" - "os/signal" - "path/filepath" - "syscall" - "time" - - "golang.zx2c4.com/irc/hbot" -) - -func main() { - rand.Seed(time.Now().UnixNano()) - - shortlink := NewShortLink(filepath.Join(os.Getenv("STATE_DIRECTORY"), "links.txt")) - http.HandleFunc("/l/", shortlink.HandleRequest) - listener, err := net.FileListener(os.Stdin) - if err != nil { - log.Fatal(err) - } - go func() { - err = http.Serve(listener, nil) - if err != nil { - log.Fatal(err) - } - }() - - feeds := NewCgitFeedMonitorer(time.Second * 10) - feeds.AddFeed("https://git.zx2c4.com/wireguard-linux/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-tools/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-linux-compat/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-windows/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-go/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-freebsd/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-openbsd/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-android/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-nt/") - feeds.AddFeed("https://git.zx2c4.com/android-wireguard-module-builder/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-apple/") - feeds.AddFeed("https://git.zx2c4.com/wireguard-rs/") - feeds.AddFeed("https://git.zx2c4.com/wintun/") - feeds.AddFeed("https://git.zx2c4.com/wg-dynamic/") - - const channel = "#wireguard" - bot := hbot.NewBot(&hbot.Config{ - Host: "irc.libera.chat:6697", - Nick: "WurGurBoo", - Realname: "Your Friendly Neighborhood WurGur Bot", - Channels: []string{channel}, - Logger: hbot.Logger{Verbosef: log.Printf, Errorf: log.Printf}, - Password: os.Getenv("WURGURBOO_PASSWORD"), - }) - banter := NewBanter(channel, bot.Nick()) - bot.AddTrigger(hbot.Trigger{ - Condition: func(b *hbot.Bot, m *hbot.Message) bool { return m.Command == "PRIVMSG" }, - Action: banter.Handle, - }) - ntDriverBuilderNotifier := NewNtDriverBuilderNotifier(channel, bot) - http.HandleFunc("/nt-driver-builder-notify", ntDriverBuilderNotifier.HandleRequest) - - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) - go func() { - for range c { - bot.Close() - shortlink.SaveToDisk() - os.Exit(0) - } - }() - - type seenCommit struct { - repo string - subject string - } - seenCommits := make(map[seenCommit]string, 4096) - - go func() { - for commit := range feeds.Updates() { - <-bot.Joined() - sc := seenCommit{commit.RepoTitle, commit.Commit.Title} - if short, ok := seenCommits[sc]; ok { - log.Printf("Updated commit %s in %s", commit.Commit.ID, commit.RepoTitle) - shortlink.Set(short, commit.Commit.Link.Href, true) - continue - } - log.Printf("New commit %s in %s", commit.Commit.ID, commit.RepoTitle) - short := shortlink.New(commit.Commit.Link.Href) - seenCommits[sc] = short - bot.Action(channel, fmt.Sprintf("found a new commit in \x0303%s\x0f - \x0306%s\x0f - %s", - sc.repo, sc.subject, - fmt.Sprintf("https://w-g.pw/l/%s", short)), - ) - } - }() - - for { - bot.Run() - time.Sleep(time.Second * 5) - } -} diff --git a/cmd/wurgurboo/nt-driver-builder.go b/cmd/wurgurboo/nt-driver-builder.go deleted file mode 100644 index 1c7eed7..0000000 --- a/cmd/wurgurboo/nt-driver-builder.go +++ /dev/null @@ -1,57 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "crypto/hmac" - "encoding/base64" - "fmt" - "net/http" - "os" - - "golang.zx2c4.com/irc/hbot" -) - -type NtDriverBuilderNotifier struct { - bot *hbot.Bot - channel string - secret [32]byte -} - -func NewNtDriverBuilderNotifier(channel string, bot *hbot.Bot) *NtDriverBuilderNotifier { - notifier := new(NtDriverBuilderNotifier) - secret, err := base64.StdEncoding.DecodeString(os.Getenv("WURGURBOO_NTDRIVERBUILDERNOTIFIER_SECRET")) - if err != nil || len(secret) != 32 { - return notifier // Silently disable on failure - } - copy(notifier.secret[:], secret) - notifier.bot = bot - notifier.channel = channel - return notifier -} - -func isValidNotificationString(s string) bool { - for _, c := range []byte(s) { - if c < ' ' || c > '~' { - return false - } - } - return len(s) >= 3 && len(s) < 200 -} - -func (notifier *NtDriverBuilderNotifier) HandleRequest(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Server", "WurGurBoo/1.0") - secret, _ := base64.StdEncoding.DecodeString(r.Header.Get("Secret")) - driver, action := r.Header.Get("Driver"), r.Header.Get("Action") - if r.Method != http.MethodPost || notifier.bot == nil || !hmac.Equal(secret, notifier.secret[:]) || - !isValidNotificationString(driver) || !isValidNotificationString(action) { - http.Redirect(w, r, "https://www.wireguard.com/", 302) - return - } - notifier.bot.Msg(notifier.channel, - fmt.Sprintf("rozmansi, zx2c4: \x0303nt-driver-builder\x0f %s \x0306%s\x0f", - action, driver)) -} diff --git a/cmd/wurgurboo/shortlink.go b/cmd/wurgurboo/shortlink.go deleted file mode 100644 index af15064..0000000 --- a/cmd/wurgurboo/shortlink.go +++ /dev/null @@ -1,120 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 - * - * Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved. - */ - -package main - -import ( - "bufio" - "crypto/rand" - "encoding/base64" - "fmt" - "log" - "net/http" - "os" - "path" - "strings" - "sync" - "time" -) - -type ShortLink struct { - mu sync.RWMutex - diskCachePath string - links map[string]string - dirty bool - cacher *time.Timer -} - -func NewShortLink(diskCachePath string) *ShortLink { - sl := &ShortLink{ - diskCachePath: diskCachePath, - links: make(map[string]string, 4096), - } - f, err := os.Open(sl.diskCachePath) - if err == nil { - defer f.Close() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - parts := strings.SplitN(scanner.Text(), "\t", 2) - if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { - continue - } - sl.links[parts[0]] = parts[1] - } - } - return sl -} - -func (sl *ShortLink) Set(k, v string, overwrite bool) bool { - sl.mu.Lock() - defer sl.mu.Unlock() - if wantV, ok := sl.links[k]; ok && (!overwrite || wantV == v) { - return false - } - sl.links[k] = v - if !sl.dirty { - sl.dirty = true - time.AfterFunc(time.Second*10, sl.SaveToDisk) - } - return true -} - -func (sl *ShortLink) New(v string) string { - for { - var bytes [3]byte - _, err := rand.Read(bytes[:]) - if err != nil { - log.Panicf("RNG is broken: %v", err) - } - k := base64.RawURLEncoding.EncodeToString(bytes[:]) - sl.mu.RLock() - _, exists := sl.links[k] - sl.mu.RUnlock() - if !exists && sl.Set(k, v, false) { - return k - } - } -} - -func (sl *ShortLink) HandleRequest(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Server", "WurGurBoo/1.0") - target := "https://www.wireguard.com/" - wantL, key := path.Split(r.URL.Path) - if wantL == "/l/" { - sl.mu.RLock() - v, ok := sl.links[key] - sl.mu.RUnlock() - if ok { - target = v - } - } - http.Redirect(w, r, target, 302) -} - -func (sl *ShortLink) SaveToDisk() { - sl.mu.Lock() - dirty := sl.dirty - sl.dirty = false - sl.mu.Unlock() - if !dirty { - return - } - f, err := os.Create(sl.diskCachePath + ".tmp") - if err != nil { - log.Printf("Unable to backup short link db: %v", err) - return - } - defer f.Close() - sl.mu.RLock() - for k, v := range sl.links { - fmt.Fprintf(f, "%s\t%s\n", k, v) - } - sl.mu.RUnlock() - err = os.Rename(f.Name(), sl.diskCachePath) - if err != nil { - log.Printf("Unable to backup short link db: %v", err) - return - } -} diff --git a/hbot/commands.go b/commands.go similarity index 100% rename from hbot/commands.go rename to commands.go diff --git a/go.mod b/go.mod index b177576..9bc2654 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module golang.zx2c4.com/irc +module code.laidback.moe/hbot go 1.17 diff --git a/hbot/hbot.go b/hbot.go similarity index 92% rename from hbot/hbot.go rename to hbot.go index 60fbb08..e37728e 100644 --- a/hbot/hbot.go +++ b/hbot.go @@ -51,6 +51,7 @@ type Config struct { Password string Channels []string SASL bool + NickServ bool MsgSafetyBuffer bool // Set it if long messages get truncated on the receiving end Dial func(network, addr string) (net.Conn, error) // An optional custom function for connecting to the server ThrottleDelay time.Duration // Duration to wait between sending of messages to avoid being kicked by the server for flooding (default 200ms) @@ -132,6 +133,17 @@ func (bot *Bot) standardRegistration() { bot.SetNick(bot.config.Nick) } +// nickservAuthenticate performs NickServ authentication +func (bot *Bot) nickservAuthenticate(pass string) { + bot.Send("CAP LS") + if bot.config.Password != "" { + bot.Send("PRIVMSG NickServ IDENTIFY " + bot.config.Password) + } + bot.config.Logger.Verbosef("Identifying with NickServ") + bot.sendUserCommand(bot.config.User, bot.config.Realname, "0") + bot.SetNick(bot.config.Nick) +} + // Set username, real name, and mode func (bot *Bot) sendUserCommand(user, realname, mode string) { bot.Send(fmt.Sprintf("USER %s %s * :%s", user, mode, realname)) @@ -153,6 +165,10 @@ func (bot *Bot) connect(host string) error { _, portStr, _ := net.SplitHostPort(host) port, _ := strconv.Atoi(portStr) switch port { + case 194: + // This was the original IRC port + // But nowadays it's not used because it is a privileged port + bot.config.UseTLS = NoTLS case 6667: bot.config.UseTLS = NoTLS case 6697: @@ -234,6 +250,8 @@ func (bot *Bot) Run() { go bot.handleOutgoingMessages() if bot.config.SASL { bot.saslAuthenticate(bot.config.Nick, bot.config.Password) + } else if bot.config.NickServ { + bot.nickservAuthenticate(bot.config.Password) } else { bot.standardRegistration() } diff --git a/hbot/internaltriggers.go b/internaltriggers.go similarity index 100% rename from hbot/internaltriggers.go rename to internaltriggers.go diff --git a/hbot/irc_caps.go b/irc_caps.go similarity index 100% rename from hbot/irc_caps.go rename to irc_caps.go diff --git a/hbot/message.go b/message.go similarity index 100% rename from hbot/message.go rename to message.go diff --git a/hbot/message_test.go b/message_test.go similarity index 100% rename from hbot/message_test.go rename to message_test.go