noot
This commit is contained in:
commit
2aa412afee
|
@ -0,0 +1 @@
|
|||
.env
|
|
@ -0,0 +1 @@
|
|||
.env
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
"github.com/lmittmann/tint"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
"tuxpa.in/a/irc/pkg/ircconn"
|
||||
"tuxpa.in/a/irc/pkg/ircmw"
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
"tuxpa.in/a/irc/plugins/auth"
|
||||
"tuxpa.in/a/irc/plugins/useful"
|
||||
)
|
||||
|
||||
func exec(log *slog.Logger) error {
|
||||
tlsConfig := &tls.Config{}
|
||||
conn, err := tls.Dial("tcp", "irc.libera.chat:6697", tlsConfig)
|
||||
//conn, err := net.Dial("tcp", "irc.libera.chat:6667")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
irc := ircconn.New(log, conn, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
name := os.Getenv("LAIN_NICKNAME")
|
||||
saslPassword := os.Getenv("LAIN_PASSWORD")
|
||||
c := &ircmw.Capabilities{}
|
||||
handler := ircv3.Chain(
|
||||
func(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
log.Info("in <<", "msg", m.String())
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
},
|
||||
c.Middleware,
|
||||
(&auth.SaslPlain{
|
||||
Username: name,
|
||||
Password: saslPassword,
|
||||
}).Middleware,
|
||||
(&auth.Nick{Nick: name}).Middleware,
|
||||
(&auth.User{Username: "lain", Realname: "lain a", Hostname: "wired", Server: "wired"}).Middleware,
|
||||
ircmw.CapabilityServerTime,
|
||||
(&useful.Autojoin{Channels: []string{"#lainmaxxing"}}).Middleware,
|
||||
).Handler(ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
}))
|
||||
|
||||
err = irc.Serve(ctx, handler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
lain := fx.New(
|
||||
fx.Provide(func() *slog.Logger {
|
||||
return slog.New(
|
||||
tint.NewHandler(os.Stderr, &tint.Options{
|
||||
Level: slog.LevelDebug,
|
||||
TimeFormat: time.Kitchen,
|
||||
}))
|
||||
}),
|
||||
fx.WithLogger(func(s *slog.Logger) fxevent.Logger {
|
||||
return &fxevent.SlogLogger{Logger: s}
|
||||
}),
|
||||
fx.Invoke(exec),
|
||||
)
|
||||
lain.Run()
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
module tuxpa.in/a/irc
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lmittmann/tint v1.0.4
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/valyala/bytebufferpool v1.0.0
|
||||
go.uber.org/fx v1.21.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.uber.org/dig v1.17.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
|
||||
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.21.1 h1:RqBh3cYdzZS0uqwVeEjOX2p73dddLpym315myy/Bpb0=
|
||||
go.uber.org/fx v1.21.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,69 @@
|
|||
package ircconn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"tuxpa.in/a/irc/pkg/ircdecoder"
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
w io.Writer
|
||||
r io.Reader
|
||||
log *slog.Logger
|
||||
muWrite sync.Mutex
|
||||
}
|
||||
|
||||
func New(log *slog.Logger, w io.Writer, r io.Reader) *Conn {
|
||||
return &Conn{
|
||||
log: log,
|
||||
w: w,
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
// while serve is running, the conn owns the reader.
|
||||
func (c *Conn) Serve(ctx context.Context, h ircv3.Handler) error {
|
||||
// once serve is called, we call with an empty message.
|
||||
h.Handle(ctx, c, &ircv3.Message{})
|
||||
dec := &ircdecoder.Decoder{}
|
||||
r := c.r
|
||||
r = bufio.NewReaderSize(c.r, 10240)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
msg := &ircv3.Message{}
|
||||
r = io.LimitReader(r, 8191+512)
|
||||
err := dec.Decode(r, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Handle(ctx, c, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) WriteMessage(msg *ircv3.Message) error {
|
||||
b := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(b)
|
||||
err := msg.Encode(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
c.muWrite.Lock()
|
||||
defer c.muWrite.Unlock()
|
||||
c.log.Info("out >", "msg", msg.String())
|
||||
_, err = b.WriteTo(c.w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package ircdecoder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
type Decoder struct {
|
||||
}
|
||||
|
||||
func (d *Decoder) readByte(r io.Reader) (byte, error) {
|
||||
var o [1]byte
|
||||
if c, ok := r.(io.ByteReader); ok {
|
||||
return c.ReadByte()
|
||||
}
|
||||
_, err := io.ReadFull(r, o[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return o[0], nil
|
||||
}
|
||||
|
||||
// read a message from the stream
|
||||
func (d *Decoder) Decode(r io.Reader, msg *ircv3.Message) error {
|
||||
return d.decode(r, msg)
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeTags(r io.Reader, msg *ircv3.Message) error {
|
||||
// we assume we have already read the @
|
||||
if msg.Tags == nil {
|
||||
msg.Tags = make(ircv3.Tags)
|
||||
}
|
||||
|
||||
kb := new(strings.Builder)
|
||||
vb := new(strings.Builder)
|
||||
readingValue := false
|
||||
for {
|
||||
// keep reading until space
|
||||
b, err := d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b == ';' {
|
||||
msg.Tags.Set(kb.String(), vb.String())
|
||||
readingValue = false
|
||||
continue
|
||||
} else if b == '=' {
|
||||
readingValue = true
|
||||
continue
|
||||
}
|
||||
if b == 0x20 {
|
||||
kstr := kb.String()
|
||||
if !utf8.ValidString(kstr) {
|
||||
return fmt.Errorf("non utf-8 tag key")
|
||||
}
|
||||
msg.Tags.Set(kstr, ircv3.UnescapeTagValue(vb.String()))
|
||||
readingValue = false
|
||||
break
|
||||
}
|
||||
if readingValue {
|
||||
vb.WriteByte(b)
|
||||
} else {
|
||||
// TODO: technically we should check the validity of key bytes, not scan for utf8 at the end
|
||||
// <key_name> ::= <non-empty sequence of ascii letters, digits, hyphens ('-')>
|
||||
kb.WriteByte(b)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeSource(r io.Reader, msg *ircv3.Message) error {
|
||||
// we assume we have already read the :
|
||||
buf := new(strings.Builder)
|
||||
for {
|
||||
// keep reading until space
|
||||
b, err := d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b == 0x20 {
|
||||
break
|
||||
}
|
||||
buf.WriteByte(b)
|
||||
}
|
||||
nuh, err := ircv3.ParseNUH(buf.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Source = &nuh
|
||||
return nil
|
||||
}
|
||||
|
||||
// read a message from the stream
|
||||
func (d *Decoder) decode(r io.Reader, msg *ircv3.Message) error {
|
||||
b, err := d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '@':
|
||||
if err := d.decodeTags(r, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b == ':' {
|
||||
if err := d.decodeSource(r, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case ':':
|
||||
if err := d.decodeSource(r, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = d.readByte(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
}
|
||||
cb := new(strings.Builder)
|
||||
// at this point we've no matter waht read the first byte of the command in b
|
||||
cb.WriteByte(b)
|
||||
// add a limit reader for the irc size limit
|
||||
r = io.LimitReader(r, 511)
|
||||
// read until first space
|
||||
for {
|
||||
b, err := d.readByte(r)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
msg.Command = cb.String()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if b == 0x20 {
|
||||
break
|
||||
}
|
||||
cb.WriteByte(b)
|
||||
}
|
||||
msg.Command = cb.String()
|
||||
cb.Reset()
|
||||
// now read the params
|
||||
|
||||
var trailing bool
|
||||
var lastCr bool
|
||||
for {
|
||||
b, err := d.readByte(r)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
if cb.Len() > 0 {
|
||||
msg.Params = append(msg.Params, cb.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if cb.Len() == 0 {
|
||||
if b == ':' {
|
||||
trailing = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !trailing {
|
||||
if b == 0x20 {
|
||||
msg.Params = append(msg.Params, cb.String())
|
||||
cb.Reset()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if b == '\r' {
|
||||
lastCr = true
|
||||
continue
|
||||
}
|
||||
if lastCr {
|
||||
if b == '\n' {
|
||||
msg.Params = append(msg.Params, cb.String())
|
||||
return nil
|
||||
} else {
|
||||
cb.WriteByte('\r')
|
||||
}
|
||||
}
|
||||
cb.WriteByte(b)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package ircdecoder_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"tuxpa.in/a/irc/pkg/ircdecoder"
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
func TestDecoder(t *testing.T) {
|
||||
dec := ircdecoder.Decoder{}
|
||||
|
||||
buf := strings.NewReader("@some :source HELLO")
|
||||
msg := new(ircv3.Message)
|
||||
err := dec.Decode(buf, msg)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, "HELLO", msg.Command)
|
||||
require.EqualValues(t, "", msg.Tags["some"])
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package ircmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
type Capabilities struct {
|
||||
pending int
|
||||
}
|
||||
|
||||
var capabilitiesKey struct{}
|
||||
|
||||
func AddPending(ctx context.Context, i int) {
|
||||
val, ok := ctx.Value(capabilitiesKey).(*Capabilities)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
val.pending += i
|
||||
}
|
||||
|
||||
func (c *Capabilities) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
ctx = context.WithValue(ctx, capabilitiesKey, c)
|
||||
if m.Command == "" {
|
||||
c.pending++
|
||||
w.WriteMessage(ircv3.NewMessage("CAP", "LS", "302"))
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "LS" {
|
||||
c.pending--
|
||||
}
|
||||
if c.pending == 0 {
|
||||
c.pending = -1
|
||||
w.WriteMessage(ircv3.NewMessage("CAP", "END"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func CapabilityExchange(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
||||
|
||||
var keyServerTime struct{}
|
||||
|
||||
func ServerTime(ctx context.Context) time.Time {
|
||||
val, ok := ctx.Value(keyServerTime).(time.Time)
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func CapabilityServerTime(next ircv3.Handler) ircv3.Handler {
|
||||
enabled := false
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "LS" {
|
||||
if strings.Contains(m.Param(2), "server-time") {
|
||||
AddPending(ctx, 1)
|
||||
w.WriteMessage(ircv3.NewMessage("CAP", "REQ", "server-time"))
|
||||
}
|
||||
}
|
||||
if m.Command == "CAP" && m.Param(1) == "ACK" && m.Param(2) == "server-time" {
|
||||
AddPending(ctx, -1)
|
||||
enabled = true
|
||||
}
|
||||
if enabled {
|
||||
tString := m.Tags.Get("time")
|
||||
if tString != "" {
|
||||
parsedTime, err := time.Parse("2006-01-02T15:04:05.000Z", tString)
|
||||
if err == nil {
|
||||
ctx = context.WithValue(ctx, keyServerTime, parsedTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package ircv3
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
Handle(ctx context.Context, w MessageWriter, m *Message)
|
||||
}
|
||||
type HandlerFunc func(ctx context.Context, w MessageWriter, m *Message)
|
||||
|
||||
func (h HandlerFunc) Handle(ctx context.Context, w MessageWriter, m *Message) {
|
||||
h(ctx, w, m)
|
||||
}
|
||||
|
||||
type MessageWriter interface {
|
||||
WriteMessage(msg *Message) error
|
||||
}
|
||||
|
||||
type Middleware func(next Handler) Handler
|
||||
|
||||
type Middlewares []func(Handler) Handler
|
||||
|
||||
// Chain returns a Middlewares type from a slice of middleware handlers.
|
||||
func Chain(middlewares ...func(Handler) Handler) Middlewares {
|
||||
return Middlewares(middlewares)
|
||||
}
|
||||
|
||||
// Handler builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) Handler(h Handler) Handler {
|
||||
return &ChainHandler{h, chain(mws, h), mws}
|
||||
}
|
||||
|
||||
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
|
||||
// with `h http.Handler` as the final handler.
|
||||
func (mws Middlewares) HandlerFunc(h HandlerFunc) Handler {
|
||||
return &ChainHandler{h, chain(mws, h), mws}
|
||||
}
|
||||
|
||||
// ChainHandler is a http.Handler with support for handler composition and
|
||||
// execution.
|
||||
type ChainHandler struct {
|
||||
Endpoint Handler
|
||||
chain Handler
|
||||
Middlewares Middlewares
|
||||
}
|
||||
|
||||
func (c *ChainHandler) Handle(ctx context.Context, w MessageWriter, m *Message) {
|
||||
c.chain.Handle(ctx, w, m)
|
||||
}
|
||||
|
||||
// chain builds a http.Handler composed of an inline middleware stack and endpoint
|
||||
// handler in the order they are passed.
|
||||
func chain(middlewares []func(Handler) Handler, endpoint Handler) Handler {
|
||||
// Return ahead of time if there aren't any middlewares for the chain
|
||||
if len(middlewares) == 0 {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// Wrap the end handler with the middleware chain
|
||||
h := middlewares[len(middlewares)-1](endpoint)
|
||||
for i := len(middlewares) - 2; i >= 0; i-- {
|
||||
h = middlewares[i](h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package ircv3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Source *NUH
|
||||
Command string
|
||||
Params []string
|
||||
Tags Tags
|
||||
}
|
||||
|
||||
func NewMessage(command string, params ...string) *Message {
|
||||
return &Message{
|
||||
Source: nil,
|
||||
Command: command,
|
||||
Params: params,
|
||||
Tags: make(Tags),
|
||||
}
|
||||
}
|
||||
func (m *Message) SetSource(nuh *NUH) *Message {
|
||||
m.Source = nuh
|
||||
return m
|
||||
}
|
||||
func (m *Message) Param(i int) string {
|
||||
if len(m.Params) > i {
|
||||
return m.Params[i]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (msg *Message) String() string {
|
||||
sb := new(strings.Builder)
|
||||
msg.Encode(sb)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (msg *Message) Encode(w io.Writer) error {
|
||||
if msg.Command == "" {
|
||||
return nil
|
||||
}
|
||||
if msg.Tags != nil && len(msg.Tags) > 0 {
|
||||
_, err := msg.Tags.WriteTo(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(" "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if msg.Source != nil {
|
||||
if _, err := w.Write([]byte(msg.Source.String())); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// now write command
|
||||
if _, err := w.Write([]byte(msg.Command)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return err
|
||||
}
|
||||
for idx, v := range msg.Params {
|
||||
if idx != 0 {
|
||||
if _, err := w.Write([]byte(" ")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// now write any params
|
||||
if len(msg.Params)-1 == idx && strings.Contains(v, " ") {
|
||||
if _, err := w.Write([]byte(":")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := w.Write([]byte(v)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NUH struct {
|
||||
Name string
|
||||
User string
|
||||
Host string
|
||||
}
|
||||
|
||||
var (
|
||||
MalformedNUH = errors.New("NUH is malformed")
|
||||
)
|
||||
|
||||
// ParseNUH parses a NUH source of an IRC message into its constituent parts;
|
||||
// name (nickname or server name), username, and hostname.
|
||||
func ParseNUH(in string) (out NUH, err error) {
|
||||
if len(in) == 0 {
|
||||
return out, MalformedNUH
|
||||
}
|
||||
hostStart := strings.IndexByte(in, '@')
|
||||
if hostStart != -1 {
|
||||
out.Host = in[hostStart+1:]
|
||||
in = in[:hostStart]
|
||||
}
|
||||
userStart := strings.IndexByte(in, '!')
|
||||
if userStart != -1 {
|
||||
out.User = in[userStart+1:]
|
||||
in = in[:userStart]
|
||||
}
|
||||
out.Name = in
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Canonical returns the canonical string representation of the NUH.
|
||||
func (nuh *NUH) String() (result string) {
|
||||
var out strings.Builder
|
||||
out.Grow(len(nuh.Name) + len(nuh.User) + len(nuh.Host) + 2)
|
||||
out.WriteString(nuh.Name)
|
||||
if len(nuh.User) != 0 {
|
||||
out.WriteByte('!')
|
||||
out.WriteString(nuh.User)
|
||||
}
|
||||
if len(nuh.Host) != 0 {
|
||||
out.WriteByte('@')
|
||||
out.WriteString(nuh.Host)
|
||||
}
|
||||
return out.String()
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package ircv3
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// taken from https://github.com/ergochat/irc-go/blob/master/ircmsg/tags.go
|
||||
|
||||
var (
|
||||
// valtoescape replaces real characters with message tag escapes.
|
||||
valtoescape = strings.NewReplacer("\\", "\\\\", ";", "\\:", " ", "\\s", "\r", "\\r", "\n", "\\n")
|
||||
|
||||
escapedCharLookupTable [256]byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 256; i += 1 {
|
||||
escapedCharLookupTable[i] = byte(i)
|
||||
}
|
||||
escapedCharLookupTable[':'] = ';'
|
||||
escapedCharLookupTable['s'] = ' '
|
||||
escapedCharLookupTable['r'] = '\r'
|
||||
escapedCharLookupTable['n'] = '\n'
|
||||
}
|
||||
|
||||
// EscapeTagValue takes a value, and returns an escaped message tag value.
|
||||
func EscapeTagValue(inString string) string {
|
||||
return valtoescape.Replace(inString)
|
||||
}
|
||||
|
||||
// UnescapeTagValue takes an escaped message tag value, and returns the raw value.
|
||||
func UnescapeTagValue(inString string) string {
|
||||
// buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
|
||||
var buf strings.Builder
|
||||
remainder := inString
|
||||
for {
|
||||
backslashPos := strings.IndexByte(remainder, '\\')
|
||||
|
||||
if backslashPos == -1 {
|
||||
if buf.Len() == 0 {
|
||||
return inString
|
||||
} else {
|
||||
buf.WriteString(remainder)
|
||||
break
|
||||
}
|
||||
} else if backslashPos == len(remainder)-1 {
|
||||
// trailing backslash, which we strip
|
||||
if buf.Len() == 0 {
|
||||
return inString[:len(inString)-1]
|
||||
} else {
|
||||
buf.WriteString(remainder[:len(remainder)-1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// non-trailing backslash detected; we're now on the slowpath
|
||||
// where we modify the string
|
||||
if buf.Len() == 0 {
|
||||
buf.Grow(len(inString)) // just an optimization
|
||||
}
|
||||
buf.WriteString(remainder[:backslashPos])
|
||||
buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
|
||||
remainder = remainder[backslashPos+2:]
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// https://ircv3.net/specs/extensions/message-tags.html#rules-for-naming-message-tags
|
||||
func validateTagName(name string) bool {
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
if name[0] == '+' {
|
||||
name = name[1:]
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
// let's err on the side of leniency here; allow -./ (45-47) in any position
|
||||
for i := 0; i < len(name); i++ {
|
||||
c := name[i]
|
||||
if !(('-' <= c && c <= '/') || ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// "Tag values MUST be encoded as UTF8."
|
||||
func ValidateTagValue(value string) bool {
|
||||
return utf8.ValidString(value)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package ircv3
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tags are stored as strings
|
||||
type Tags map[string]string
|
||||
|
||||
func (h Tags) Set(key, value string) {
|
||||
h[key] = value
|
||||
}
|
||||
|
||||
func (h Tags) Get(key string) string {
|
||||
val, ok := h[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (h Tags) Keys() []string {
|
||||
o := make([]string, 0, len(h))
|
||||
for k := range h {
|
||||
o = append(o, k)
|
||||
}
|
||||
return o
|
||||
}
|
||||
func (h Tags) ClientOnlyKeys() []string {
|
||||
o := make([]string, 0)
|
||||
for k := range h {
|
||||
if IsClientOnly(k) {
|
||||
o = append(o, k)
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func IsClientOnly(xs string) bool {
|
||||
return strings.HasPrefix(xs, "+")
|
||||
}
|
||||
|
||||
func (h Tags) WriteTo(w io.Writer) (int64, error) {
|
||||
var nn int64
|
||||
n, err := w.Write([]byte{'@'})
|
||||
nn += int64(n)
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
firstTag := true
|
||||
for k, v := range h {
|
||||
if !firstTag {
|
||||
n, err = w.Write([]byte(";"))
|
||||
nn += int64(n)
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
}
|
||||
firstTag = false
|
||||
n, err := w.Write([]byte(k))
|
||||
nn += int64(n)
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
if len(v) > 0 {
|
||||
n, err = w.Write([]byte("=" + EscapeTagValue(v)))
|
||||
nn += int64(n)
|
||||
if err != nil {
|
||||
return nn, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nn, nil
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"tuxpa.in/a/irc/pkg/ircmw"
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
type SaslPlain struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (saslplain *SaslPlain) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
if m.Command == "" {
|
||||
ircmw.AddPending(ctx, 1)
|
||||
w.WriteMessage(ircv3.NewMessage("CAP", "REQ", "sasl"))
|
||||
}
|
||||
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "ACK" && m.Param(2) == "sasl" {
|
||||
w.WriteMessage(ircv3.NewMessage("AUTHENTICATE", "PLAIN"))
|
||||
}
|
||||
if m.Command == "AUTHENTICATE" && m.Param(0) == "+" {
|
||||
w.WriteMessage(ircv3.NewMessage("AUTHENTICATE", base64.StdEncoding.EncodeToString([]byte(
|
||||
saslplain.Username+string([]byte{0})+
|
||||
saslplain.Username+string([]byte{0})+
|
||||
saslplain.Password,
|
||||
))))
|
||||
}
|
||||
switch m.Command {
|
||||
case "903", "904", "905", "906", "907":
|
||||
ircmw.AddPending(ctx, -1)
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Realname string
|
||||
Hostname string
|
||||
Server string
|
||||
}
|
||||
|
||||
func (u *User) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
if m.Command == "" {
|
||||
w.WriteMessage(ircv3.NewMessage("USER", u.Username, u.Hostname, u.Server, u.Realname))
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
||||
|
||||
type Nick struct {
|
||||
Nick string
|
||||
}
|
||||
|
||||
func (u *Nick) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
if m.Command == "" {
|
||||
w.WriteMessage(ircv3.NewMessage("NICK", u.Nick))
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package useful
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"tuxpa.in/a/irc/pkg/ircv3"
|
||||
)
|
||||
|
||||
type Autojoin struct {
|
||||
Channels []string
|
||||
}
|
||||
|
||||
func (u *Autojoin) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||
if m.Command == "005" {
|
||||
w.WriteMessage(ircv3.NewMessage("JOIN", strings.Join(u.Channels, ",")))
|
||||
}
|
||||
next.Handle(ctx, w, m)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue