This commit is contained in:
parent
d450c6cc5f
commit
53ba996ec9
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2014 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
Copyright (c) 2014 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
2
NEWS
2
NEWS
@ -37,6 +37,8 @@
|
|||||||
|
|
||||||
* Added a Go port of xD called xS
|
* Added a Go port of xD called xS
|
||||||
|
|
||||||
|
* Added a simple notifier called xN
|
||||||
|
|
||||||
|
|
||||||
1.5.0 (2021-12-21) "The Show Must Go On"
|
1.5.0 (2021-12-21) "The Show Must Go On"
|
||||||
|
|
||||||
|
@ -56,6 +56,10 @@ https://github.com/kiwiirc/webircgateway[].
|
|||||||
Any further development, such as P10 or TS6 linking for IRC services,
|
Any further development, such as P10 or TS6 linking for IRC services,
|
||||||
or plugin support for arbitrary bridges, will happen here.
|
or plugin support for arbitrary bridges, will happen here.
|
||||||
|
|
||||||
|
xN
|
||||||
|
--
|
||||||
|
The IRC notifier, should you ever need to send automated messages from a script.
|
||||||
|
|
||||||
xB
|
xB
|
||||||
--
|
--
|
||||||
The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
|
The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
|
||||||
|
3
xN/.gitignore
vendored
Normal file
3
xN/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/xN
|
||||||
|
/xN.1
|
||||||
|
/irc.go
|
19
xN/Makefile
Normal file
19
xN/Makefile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.POSIX:
|
||||||
|
.SUFFIXES:
|
||||||
|
AWK = env LC_ALL=C awk
|
||||||
|
|
||||||
|
outputs = irc.go xN xN.1
|
||||||
|
all: $(outputs)
|
||||||
|
|
||||||
|
# If we want to keep module dependencies separate, we don't have many options.
|
||||||
|
# Symlinking seems to work good enough.
|
||||||
|
irc.go: ../xS/irc.go
|
||||||
|
ln -sf ../xS/irc.go $@
|
||||||
|
|
||||||
|
xN: xN.go ../xK-version irc.go
|
||||||
|
go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@
|
||||||
|
xN.1: ../xK-version ../liberty/tools/asciiman.awk xN.adoc
|
||||||
|
env "asciidoc-release-version=$$(cat ../xK-version)" \
|
||||||
|
$(AWK) -f ../liberty/tools/asciiman.awk xN.adoc > $@
|
||||||
|
clean:
|
||||||
|
rm -f $(outputs)
|
86
xN/xN.adoc
Normal file
86
xN/xN.adoc
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
xN(1)
|
||||||
|
=====
|
||||||
|
:doctype: manpage
|
||||||
|
:manmanual: xK Manual
|
||||||
|
:mansource: xK {release-version}
|
||||||
|
|
||||||
|
Name
|
||||||
|
----
|
||||||
|
xN - IRC notifier
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
--------
|
||||||
|
*xN* [_OPTION_]... IRC-URL...
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
*xN* is a simple IRC notifier, sending the text it receives on its standard
|
||||||
|
input to all IRC targets specified by its command line arguments.
|
||||||
|
|
||||||
|
The input text is forced to validate as UTF-8, and it is _not_ split
|
||||||
|
automatically to comply with the maximum IRC message length.
|
||||||
|
Thus, make sure to make the lines short, or they will be trimmed by the servers.
|
||||||
|
|
||||||
|
*xN* does not attempt to appease flood detectors.
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
*-debug*::
|
||||||
|
Print incoming IRC traffic, which may help in debugging any issues.
|
||||||
|
|
||||||
|
*-version*::
|
||||||
|
Output version information and exit.
|
||||||
|
|
||||||
|
URL format
|
||||||
|
----------
|
||||||
|
*xN* accepts URLs describing IRC users and channels, roughly as specified by
|
||||||
|
the Internet Draft _draft-butcher-irc-url-04.txt_. Note, however, that close
|
||||||
|
to no validation is done on these, and you should not pass URLs from untrusted
|
||||||
|
sources, so as to avoid command or parameter injection.
|
||||||
|
|
||||||
|
Any provided username will be propagated to the nickname, username,
|
||||||
|
and realname. The default value for these is the name of your system user.
|
||||||
|
|
||||||
|
As an extension, you may use the following options:
|
||||||
|
|
||||||
|
*skipjoin*::
|
||||||
|
Do not JOIN channels before sending messages to them.
|
||||||
|
This requires channels to allow external messages
|
||||||
|
(which are disabled by channel mode *+n*).
|
||||||
|
*usenotice*::
|
||||||
|
Send a NOTICE rather than a PRIVMSG, which distinguishes automated messages,
|
||||||
|
and is more appropriate for bots.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
$ uptime | xN 'irc://uptime@localhost/%23watch?skipjoin&usenotice'
|
||||||
|
|
||||||
|
Send *uptime*(1) information as an external notice to channel *#watch*
|
||||||
|
on the local server, using the standard port *6667*.
|
||||||
|
|
||||||
|
$ fortune -s | xN ircs://ohayou@irc.libera.chat/john,isuser
|
||||||
|
|
||||||
|
Greet user *john* with a fortune for this day. In compliance with _RFC 7194_,
|
||||||
|
the default TLS port is assumed to be *6697*.
|
||||||
|
|
||||||
|
$ xN 'ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456' <<EOF
|
||||||
|
The red fox trots quietly at midnight.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
Connect over TLS to *irc.cia.gov* on port *1337*, use *Password123*
|
||||||
|
as the server password, register as user *agent*,
|
||||||
|
join channel *#hq* using the channel key *123456*,
|
||||||
|
and send a very secret message.
|
||||||
|
|
||||||
|
Reporting bugs
|
||||||
|
--------------
|
||||||
|
Use https://git.janouch.name/p/xK to report bugs, request features,
|
||||||
|
or submit pull requests.
|
||||||
|
|
||||||
|
See also
|
||||||
|
--------
|
||||||
|
_Uniform Resource Locator Schemes for Internet Relay Chat Entities_,
|
||||||
|
https://datatracker.ietf.org/doc/html/draft-butcher-irc-url-04[].
|
||||||
|
|
||||||
|
_Default Port for Internet Relay Chat (IRC) via TLS/SSL_,
|
||||||
|
https://datatracker.ietf.org/doc/html/rfc7194[].
|
279
xN/xN.go
Normal file
279
xN/xN.go
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
// xN is a simple IRC notifier.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectName = "xN"
|
||||||
|
|
||||||
|
var projectVersion = "?"
|
||||||
|
|
||||||
|
var debugMode = false
|
||||||
|
|
||||||
|
type parameters struct {
|
||||||
|
conn net.Conn // underlying network connection
|
||||||
|
username string // nickname + username + realname
|
||||||
|
password string // server password
|
||||||
|
target string // where to send text
|
||||||
|
isuser bool // the target is a user rather than a channel
|
||||||
|
chankey string // channel key
|
||||||
|
skipjoin bool // whether to send external messages to channels
|
||||||
|
usenotice bool // whether to use NOTICE rather than PRIVMSG
|
||||||
|
message []string // lines of the message to send
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parameters) send(command string, args ...string) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(command)
|
||||||
|
for i, arg := range args {
|
||||||
|
buf.WriteRune(' ')
|
||||||
|
if i+1 == len(args) {
|
||||||
|
buf.WriteRune(':')
|
||||||
|
}
|
||||||
|
buf.WriteString(arg)
|
||||||
|
}
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
_, err := p.conn.Write(buf.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
rplWELCOME = "001"
|
||||||
|
errNICKNAMEINUSE = "433"
|
||||||
|
)
|
||||||
|
|
||||||
|
func notify(p *parameters) error {
|
||||||
|
// The intro should comfortably fit in the TCP send buffer whole.
|
||||||
|
if p.password != "" {
|
||||||
|
p.send("PASS", p.password)
|
||||||
|
}
|
||||||
|
p.send("USER", p.username, "0", "*", p.username)
|
||||||
|
p.send("NICK", p.username)
|
||||||
|
|
||||||
|
scanner, nickCounter, issue := bufio.NewScanner(p.conn), 1, ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
if debugMode {
|
||||||
|
log.Println(scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
m := ircParseMessage(scanner.Text())
|
||||||
|
switch m.command {
|
||||||
|
case "PING":
|
||||||
|
p.send("PONG", m.params...)
|
||||||
|
case rplWELCOME:
|
||||||
|
if !p.isuser && !p.skipjoin {
|
||||||
|
if p.chankey != "" {
|
||||||
|
p.send("JOIN", p.target, p.chankey)
|
||||||
|
} else {
|
||||||
|
p.send("JOIN", p.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, line := range p.message {
|
||||||
|
if p.usenotice {
|
||||||
|
p.send("NOTICE", p.target, line)
|
||||||
|
} else {
|
||||||
|
p.send("PRIVMSG", p.target, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.send("QUIT")
|
||||||
|
case errNICKNAMEINUSE:
|
||||||
|
p.send("NICK", fmt.Sprintf("%s%d", p.username, nickCounter))
|
||||||
|
nickCounter++
|
||||||
|
default:
|
||||||
|
// Prevent hanging on unsuccessful registrations.
|
||||||
|
numeric, _ := strconv.Atoi(m.command)
|
||||||
|
if numeric >= 400 && numeric <= 599 {
|
||||||
|
if len(m.params) > 1 {
|
||||||
|
issue = strings.Join(m.params[1:], " ")
|
||||||
|
} else {
|
||||||
|
issue = strings.Join(m.params, " ")
|
||||||
|
}
|
||||||
|
p.send("QUIT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if issue != "" {
|
||||||
|
return errors.New(issue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(rawURL string, text []byte) (
|
||||||
|
p parameters, connect func() (net.Conn, error), err error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return p, nil, err
|
||||||
|
} else if !u.IsAbs() || u.Opaque != "" {
|
||||||
|
return p, nil, errors.New("need an absolute URL")
|
||||||
|
} else if u.Path == "/" && u.Fragment != "" {
|
||||||
|
// Try to handle the common but degenerate case.
|
||||||
|
fragment := "%23" + u.Fragment
|
||||||
|
u.Fragment, u.RawFragment = "", ""
|
||||||
|
if u, err = url.Parse(u.String() + fragment); err != nil {
|
||||||
|
return p, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out registration details.
|
||||||
|
p.username = projectName
|
||||||
|
if u, _ := user.Current(); u != nil {
|
||||||
|
p.username = u.Username
|
||||||
|
}
|
||||||
|
if u.User.Username() != "" {
|
||||||
|
p.username = u.User.Username()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.password, _ = u.User.Password()
|
||||||
|
|
||||||
|
// Figure out the target, which for our intents must accept messages.
|
||||||
|
path, _ := strings.CutPrefix(u.Path, "/")
|
||||||
|
elements := strings.Split(path, ",")
|
||||||
|
if path == "" || elements[0] == "" {
|
||||||
|
return p, nil, errors.New("unspecified entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last entity type wins.
|
||||||
|
p.target, p.isuser = elements[0], false
|
||||||
|
for _, typ := range elements[1:] {
|
||||||
|
switch typ {
|
||||||
|
case "isuser":
|
||||||
|
p.isuser = true
|
||||||
|
case "ischannel":
|
||||||
|
p.isuser = false
|
||||||
|
case "isserver":
|
||||||
|
// We do not support network names, and this is the default.
|
||||||
|
default:
|
||||||
|
return p, nil, errors.New("unsupported type: " + typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.isuser {
|
||||||
|
if i := strings.IndexAny(p.target, "!@"); i != -1 {
|
||||||
|
p.target = p.target[:i]
|
||||||
|
}
|
||||||
|
} else if !strings.HasPrefix(p.target, "#") {
|
||||||
|
// TODO(p): We should consult RPL_ISUPPORT rather than guess,
|
||||||
|
// though other prefixes are rare.
|
||||||
|
p.target = "#" + p.target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the draft RFC wants these to be case-insensitive.
|
||||||
|
p.chankey = u.Query().Get("key")
|
||||||
|
// Being able to skip channel join is our own requirement and invention,
|
||||||
|
// as are notices (names taken from Travis CI configuration).
|
||||||
|
p.skipjoin = u.Query().Has("skipjoin")
|
||||||
|
p.usenotice = u.Query().Has("usenotice")
|
||||||
|
|
||||||
|
// Ensure valid LF-separated UTF-8, and split it at lines.
|
||||||
|
sanitized := strings.ReplaceAll(string([]rune(string(text))), "\r", "\n")
|
||||||
|
for _, line := range strings.Split(sanitized, "\n") {
|
||||||
|
if line != "" {
|
||||||
|
p.message = append(p.message, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, port := u.Hostname(), u.Port()
|
||||||
|
switch u.Scheme {
|
||||||
|
case "irc":
|
||||||
|
if port == "" {
|
||||||
|
port = "6667"
|
||||||
|
}
|
||||||
|
connect = func() (net.Conn, error) {
|
||||||
|
return net.Dial("tcp", net.JoinHostPort(hostname, port))
|
||||||
|
}
|
||||||
|
case "ircs":
|
||||||
|
if port == "" {
|
||||||
|
port = "6697"
|
||||||
|
}
|
||||||
|
connect = func() (net.Conn, error) {
|
||||||
|
return tls.Dial("tcp", net.JoinHostPort(hostname, port), nil)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = errors.New("unsupported scheme: " + u.Scheme)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyByURL sends the given text to the IRC server specified by a URL.
|
||||||
|
// See draft-butcher-irc-url-04.txt for the URL scheme specification
|
||||||
|
// this function loosely follows.
|
||||||
|
func notifyByURL(rawURL string, text []byte) error {
|
||||||
|
p, connect, err := parse(rawURL, text)
|
||||||
|
if p.conn, err = connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer p.conn.Close()
|
||||||
|
return notify(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
|
||||||
|
version := flag.Bool("version", false, "show version and exit")
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
f := flag.CommandLine.Output()
|
||||||
|
fmt.Fprintf(f, "Usage: %s URL...\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
fmt.Printf("%s %s\n", projectName, projectVersion)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := 0
|
||||||
|
for _, rawURL := range flag.Args() {
|
||||||
|
if err := notifyByURL(rawURL, text); err != nil {
|
||||||
|
status = 1
|
||||||
|
|
||||||
|
var ue *url.Error
|
||||||
|
if errors.As(err, &ue) {
|
||||||
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
log.Printf("notify %q: %s\n", rawURL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
98
xN/xN_test.go
Normal file
98
xN/xN_test.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
for _, rawURL := range []string{
|
||||||
|
`irc:server/channel`,
|
||||||
|
`irc://server/channel,isnetwork`,
|
||||||
|
`ircs://ssl.ircnet.io`,
|
||||||
|
`ircs://ssl.ircnet.io/`,
|
||||||
|
`http://server/path`,
|
||||||
|
} {
|
||||||
|
if _, _, err := parse(rawURL, nil); err == nil {
|
||||||
|
t.Errorf("%q should not parse\n", rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
rawURL string
|
||||||
|
p parameters
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
rawURL: `irc://uptime@localhost/%23watch?skipjoin&usenotice`,
|
||||||
|
p: parameters{
|
||||||
|
username: "uptime",
|
||||||
|
target: "#watch",
|
||||||
|
skipjoin: true,
|
||||||
|
usenotice: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rawURL: `ircs://ohayou@irc.libera.chat/john,isuser,isserver`,
|
||||||
|
p: parameters{
|
||||||
|
username: "ohayou",
|
||||||
|
target: "john",
|
||||||
|
isuser: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rawURL: `ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456`,
|
||||||
|
p: parameters{
|
||||||
|
username: "agent",
|
||||||
|
password: "Password123",
|
||||||
|
target: "#hq",
|
||||||
|
chankey: "123456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
p, _, err := parse(test.rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%q should parse, got: %s\n", test.rawURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.username != test.p.username {
|
||||||
|
t.Errorf("%q: username: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.username, test.p.username)
|
||||||
|
}
|
||||||
|
if p.password != test.p.password {
|
||||||
|
t.Errorf("%q: password: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.password, test.p.password)
|
||||||
|
}
|
||||||
|
if p.target != test.p.target {
|
||||||
|
t.Errorf("%q: target: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.target, test.p.target)
|
||||||
|
}
|
||||||
|
if p.isuser != test.p.isuser {
|
||||||
|
t.Errorf("%q: isuser: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.isuser, test.p.isuser)
|
||||||
|
}
|
||||||
|
if p.chankey != test.p.chankey {
|
||||||
|
t.Errorf("%q: chankey: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.chankey, test.p.chankey)
|
||||||
|
}
|
||||||
|
if p.skipjoin != test.p.skipjoin {
|
||||||
|
t.Errorf("%q: skipjoin: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.skipjoin, test.p.skipjoin)
|
||||||
|
}
|
||||||
|
if p.usenotice != test.p.usenotice {
|
||||||
|
t.Errorf("%q: usenotice: %v ≠ %v\n",
|
||||||
|
test.rawURL, p.usenotice, test.p.usenotice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
xS/irc.go
Normal file
132
xS/irc.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ircToLower(c byte) byte {
|
||||||
|
switch c {
|
||||||
|
case '[':
|
||||||
|
return '{'
|
||||||
|
case ']':
|
||||||
|
return '}'
|
||||||
|
case '\\':
|
||||||
|
return '|'
|
||||||
|
case '~':
|
||||||
|
return '^'
|
||||||
|
}
|
||||||
|
if c >= 'A' && c <= 'Z' {
|
||||||
|
return c + ('a' - 'A')
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircToUpper(c byte) byte {
|
||||||
|
switch c {
|
||||||
|
case '{':
|
||||||
|
return '['
|
||||||
|
case '}':
|
||||||
|
return ']'
|
||||||
|
case '|':
|
||||||
|
return '\\'
|
||||||
|
case '^':
|
||||||
|
return '~'
|
||||||
|
}
|
||||||
|
if c >= 'a' && c <= 'z' {
|
||||||
|
return c - ('a' - 'A')
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert identifier to a canonical form for case-insensitive comparisons.
|
||||||
|
// ircToUpper is used so that statically initialized maps can be in uppercase.
|
||||||
|
func ircToCanon(ident string) string {
|
||||||
|
var canon []byte
|
||||||
|
for _, c := range []byte(ident) {
|
||||||
|
canon = append(canon, ircToUpper(c))
|
||||||
|
}
|
||||||
|
return string(canon)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircEqual(s1, s2 string) bool {
|
||||||
|
return ircToCanon(s1) == ircToCanon(s2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircFnmatch(pattern string, s string) bool {
|
||||||
|
pattern, s = ircToCanon(pattern), ircToCanon(s)
|
||||||
|
// FIXME: This should not support [] ranges and handle '/' specially.
|
||||||
|
// We could translate the pattern to a regular expression.
|
||||||
|
matched, _ := filepath.Match(pattern, s)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
var reMsg = regexp.MustCompile(
|
||||||
|
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
|
||||||
|
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
tags map[string]string // IRC 3.2 message tags
|
||||||
|
nick string // optional nickname
|
||||||
|
user string // optional username
|
||||||
|
host string // optional hostname or IP address
|
||||||
|
command string // command name
|
||||||
|
params []string // arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircUnescapeMessageTag(value string) string {
|
||||||
|
var buf []byte
|
||||||
|
escape := false
|
||||||
|
for i := 0; i < len(value); i++ {
|
||||||
|
if escape {
|
||||||
|
switch value[i] {
|
||||||
|
case ':':
|
||||||
|
buf = append(buf, ';')
|
||||||
|
case 's':
|
||||||
|
buf = append(buf, ' ')
|
||||||
|
case 'r':
|
||||||
|
buf = append(buf, '\r')
|
||||||
|
case 'n':
|
||||||
|
buf = append(buf, '\n')
|
||||||
|
default:
|
||||||
|
buf = append(buf, value[i])
|
||||||
|
}
|
||||||
|
escape = false
|
||||||
|
} else if value[i] == '\\' {
|
||||||
|
escape = true
|
||||||
|
} else {
|
||||||
|
buf = append(buf, value[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircParseMessageTags(tags string, out map[string]string) {
|
||||||
|
for _, tag := range strings.Split(tags, ";") {
|
||||||
|
if tag == "" {
|
||||||
|
// Ignore empty.
|
||||||
|
} else if equal := strings.IndexByte(tag, '='); equal < 0 {
|
||||||
|
out[tag] = ""
|
||||||
|
} else {
|
||||||
|
out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ircParseMessage(line string) *message {
|
||||||
|
m := reMsg.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := message{nil, m[2], m[3], m[4], m[5], nil}
|
||||||
|
if m[1] != "" {
|
||||||
|
msg.tags = make(map[string]string)
|
||||||
|
ircParseMessageTags(m[1], msg.tags)
|
||||||
|
}
|
||||||
|
for _, x := range reArgs.FindAllString(m[6], -1) {
|
||||||
|
msg.params = append(msg.params, x[1:])
|
||||||
|
}
|
||||||
|
return &msg
|
||||||
|
}
|
126
xS/xS.go
126
xS/xS.go
@ -456,132 +456,6 @@ func (fd *floodDetector) check() bool {
|
|||||||
return count <= fd.limit
|
return count <= fd.limit
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- IRC protocol ------------------------------------------------------------
|
|
||||||
|
|
||||||
func ircToLower(c byte) byte {
|
|
||||||
switch c {
|
|
||||||
case '[':
|
|
||||||
return '{'
|
|
||||||
case ']':
|
|
||||||
return '}'
|
|
||||||
case '\\':
|
|
||||||
return '|'
|
|
||||||
case '~':
|
|
||||||
return '^'
|
|
||||||
}
|
|
||||||
if c >= 'A' && c <= 'Z' {
|
|
||||||
return c + ('a' - 'A')
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircToUpper(c byte) byte {
|
|
||||||
switch c {
|
|
||||||
case '{':
|
|
||||||
return '['
|
|
||||||
case '}':
|
|
||||||
return ']'
|
|
||||||
case '|':
|
|
||||||
return '\\'
|
|
||||||
case '^':
|
|
||||||
return '~'
|
|
||||||
}
|
|
||||||
if c >= 'a' && c <= 'z' {
|
|
||||||
return c - ('a' - 'A')
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert identifier to a canonical form for case-insensitive comparisons.
|
|
||||||
// ircToUpper is used so that statically initialized maps can be in uppercase.
|
|
||||||
func ircToCanon(ident string) string {
|
|
||||||
var canon []byte
|
|
||||||
for _, c := range []byte(ident) {
|
|
||||||
canon = append(canon, ircToUpper(c))
|
|
||||||
}
|
|
||||||
return string(canon)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircEqual(s1, s2 string) bool {
|
|
||||||
return ircToCanon(s1) == ircToCanon(s2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircFnmatch(pattern string, s string) bool {
|
|
||||||
pattern, s = ircToCanon(pattern), ircToCanon(s)
|
|
||||||
// FIXME: This should not support [] ranges and handle '/' specially.
|
|
||||||
// We could translate the pattern to a regular expression.
|
|
||||||
matched, _ := filepath.Match(pattern, s)
|
|
||||||
return matched
|
|
||||||
}
|
|
||||||
|
|
||||||
var reMsg = regexp.MustCompile(
|
|
||||||
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
|
|
||||||
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
|
|
||||||
|
|
||||||
type message struct {
|
|
||||||
tags map[string]string // IRC 3.2 message tags
|
|
||||||
nick string // optional nickname
|
|
||||||
user string // optional username
|
|
||||||
host string // optional hostname or IP address
|
|
||||||
command string // command name
|
|
||||||
params []string // arguments
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircUnescapeMessageTag(value string) string {
|
|
||||||
var buf []byte
|
|
||||||
escape := false
|
|
||||||
for i := 0; i < len(value); i++ {
|
|
||||||
if escape {
|
|
||||||
switch value[i] {
|
|
||||||
case ':':
|
|
||||||
buf = append(buf, ';')
|
|
||||||
case 's':
|
|
||||||
buf = append(buf, ' ')
|
|
||||||
case 'r':
|
|
||||||
buf = append(buf, '\r')
|
|
||||||
case 'n':
|
|
||||||
buf = append(buf, '\n')
|
|
||||||
default:
|
|
||||||
buf = append(buf, value[i])
|
|
||||||
}
|
|
||||||
escape = false
|
|
||||||
} else if value[i] == '\\' {
|
|
||||||
escape = true
|
|
||||||
} else {
|
|
||||||
buf = append(buf, value[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircParseMessageTags(tags string, out map[string]string) {
|
|
||||||
for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
|
|
||||||
if equal := strings.IndexByte(tag, '='); equal < 0 {
|
|
||||||
out[tag] = ""
|
|
||||||
} else {
|
|
||||||
out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ircParseMessage(line string) *message {
|
|
||||||
m := reMsg.FindStringSubmatch(line)
|
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := message{nil, m[2], m[3], m[4], m[5], nil}
|
|
||||||
if m[1] != "" {
|
|
||||||
msg.tags = make(map[string]string)
|
|
||||||
ircParseMessageTags(m[1], msg.tags)
|
|
||||||
}
|
|
||||||
for _, x := range reArgs.FindAllString(m[6], -1) {
|
|
||||||
msg.params = append(msg.params, x[1:])
|
|
||||||
}
|
|
||||||
return &msg
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- IRC token validation ----------------------------------------------------
|
// --- IRC token validation ----------------------------------------------------
|
||||||
|
|
||||||
// Everything as per RFC 2812
|
// Everything as per RFC 2812
|
||||||
|
Loading…
Reference in New Issue
Block a user