Add preliminary support for NickServ authentication
(remove everything else)
This commit is contained in:
15
Makefile
15
Makefile
@@ -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
|
||||
37
README.md
37
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"
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module golang.zx2c4.com/irc
|
||||
module code.laidback.moe/hbot
|
||||
|
||||
go 1.17
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user