haven/hid/main.go

1527 lines
37 KiB
Go

//
// Copyright (c) 2014 - 2018, Přemysl 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.
//
// hid is a straight-forward port of kike IRCd from C.
package main
/*
// ANSI terminal formatting, would be better if we had isatty() available
func tf(text string, ansi string) string {
return "\x1b[0;" + ansi + "m" + text + "\x1b[0m"
}
func logErrorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...)
}
func logFatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...)
os.Exit(1)
}
func logFatal(object interface{}) {
logFatalf("%s", object)
}
func getHome() (home string) {
if u, _ := user.Current(); u != nil {
home = u.HomeDir
} else {
home = os.Getenv("HOME")
}
return
}
// Only handling the simple case as that's what one mostly wants.
// TODO(p): Handle the generic case as well.
func expandTilde(path string) string {
if strings.HasPrefix(path, "~/") {
return getHome() + path[1:]
}
return path
}
func getXdgHomeDir(name, def string) string {
env := os.Getenv(name)
if env != "" && env[0] == '/' {
return env
}
return filepath.Join(getHome(), def)
}
// Retrieve all XDG base directories for configuration files
func getXdgConfigDirs() (result []string) {
home := getXdgHomeDir("XDG_CONFIG_HOME", ".config")
if home != "" {
result = append(result, home)
}
dirs := os.Getenv("XDG_CONFIG_DIRS")
if dirs == "" {
dirs = "/etc/xdg"
}
for _, path := range strings.Split(dirs, ":") {
if path != "" {
result = append(result, path)
}
}
return
}
// Read a configuration file with the given basename w/o extension
func readConfigFile(name string, output interface{}) error {
var suffix = filepath.Join(projectName, name+".json")
for _, path := range getXdgConfigDirs() {
full := filepath.Join(path, suffix)
file, err := os.Open(full)
if err != nil {
if !os.IsNotExist(err) {
return err
}
continue
}
defer file.Close()
decoder := json.NewDecoder(file)
err = decoder.Decode(output)
if err != nil {
return fmt.Errorf("%s: %s", full, err)
}
return nil
}
return errors.New("configuration file not found")
}
*/
import (
"bufio"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"
)
var debugMode = false
// --- Utilities ---------------------------------------------------------------
//
// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
// must be at least three octets long for this to work reliably, but that should
// not pose a problem in practice. We might try waiting for them.
//
// SSL2: 1xxx xxxx | xxxx xxxx | <1>
// (message length) (client hello)
// SSL3/TLS: <22> | <3> | xxxx xxxx
// (handshake)| (protocol version)
//
func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
sysconn.Read(func(fd uintptr) (done bool) {
var buf [3]byte
n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
switch {
case n == 3:
isTLS = buf[0]&0x80 != 0 && buf[2] == 1
fallthrough
case n == 2:
isTLS = buf[0] == 22 && buf[1] == 3
case n == 1:
isTLS = buf[0] == 22
case err == syscall.EAGAIN:
return false
}
return true
})
return isTLS
}
// --- Configuration -----------------------------------------------------------
// XXX: Do we really want to support default nil values?
var config = []struct {
key string // INI key
def []rune // default value, may be nil
description string // documentation
}{
// XXX: I'm not sure if Go will cooperate here.
{"pid_file", nil, "Path or name of the PID file"},
{"bind", []rune(":6667"), "Address of the IRC server"},
}
// --- Rate limiter ------------------------------------------------------------
type floodDetector struct {
interval uint // interval for the limit
limit uint // maximum number of events allowed
timestamps []int64 // timestamps of last events
pos uint // index of the oldest event
}
func newFloodDetector(interval, limit uint) *floodDetector {
return &floodDetector{
interval: interval,
limit: limit,
timestamps: make([]int64, limit+1),
pos: 0,
}
}
func (fd *floodDetector) check() bool {
now := time.Now().Unix()
fd.timestamps[fd.pos] = now
fd.pos++
if fd.pos > fd.limit {
fd.pos = 0
}
var count uint
begin := now - int64(fd.interval)
for _, ts := range fd.timestamps {
if ts >= begin {
count++
}
}
return count <= fd.limit
}
// --- IRC protocol ------------------------------------------------------------
//go:generate sh -c "./hid-gen-replies.sh > hid-replies.go < hid-replies"
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
}
// TODO: To support ALL CAPS initialization of maps, perhaps we should use
// ircToUpper instead.
// FIXME: This doesn't follow the meaning of strxfrm and perhaps should be
// renamed to ircNormalize.
func ircStrxfrm(ident string) string {
var canon []byte
for _, c := range []byte(ident) {
canon = append(canon, ircToLower(c))
}
return string(canon)
}
func ircFnmatch(pattern string, s string) bool {
pattern, s = ircStrxfrm(pattern), ircStrxfrm(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
}
// TODO: We will need to add support for IRCv3 tags.
var reMsg = regexp.MustCompile(
`^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
type message struct {
nick string // optional nickname
user string // optional username
host string // optional hostname or IP address
command string // command name
params []string // arguments
}
// Everything as per RFC 2812
const (
ircMaxNickname = 9
ircMaxHostname = 63
ircMaxChannelName = 50
ircMaxMessageLength = 510
)
// TODO: Port the IRC token validation part as needed.
const reClassSpecial = "\\[\\]\\\\`_^{|}"
var (
// Extending ASCII to the whole range of Unicode letters.
reNickname = regexp.MustCompile(
`^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
// Notably, this won't match invalid UTF-8 characters, although the
// behaviour seems to be unstated in the documentation.
reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)
reChannelName = regexp.MustCompile(`^[^\0\7\r\n ,:]+$`)
)
func ircIsValidNickname(nickname string) bool {
return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
}
func ircIsValidUsername(username string) bool {
// XXX: We really should put a limit on this
// despite the RFC not mentioning one.
return reUsername.MatchString(username)
}
func ircIsValidChannelName(name string) bool {
return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
}
// --- Clients (equals users) --------------------------------------------------
type connCloseWrite interface {
net.Conn
CloseWrite() error
}
const ircSupportedUserModes = "aiwros"
const (
ircUserModeInvisible = 1 << iota
ircUserModeRxWallops
ircUserModeRestricted
ircUserModeOperator
ircUserModeRxServerNotices
)
const (
ircCapMultiPrefix = 1 << iota
ircCapInviteNotify
ircCapEchoMessage
ircCapUserhostInNames
ircCapServerTime
)
type client struct {
transport net.Conn // underlying connection
tls *tls.Conn // TLS, if detected
conn connCloseWrite // high-level connection
inQ []byte // unprocessed input
outQ []byte // unprocessed output
reading bool // whether a reading goroutine is running
writing bool // whether a writing goroutine is running
closing bool // whether we're closing the connection
killTimer *time.Timer // hard kill timeout
opened int64 // when the connection was opened
nSentMessages uint // number of sent messages total
sentBytes int // number of sent bytes total
nReceivedMessages uint // number of received messages total
receivedBytes int // number of received bytes total
hostname string // hostname or IP shown to the network
port string // port of the peer as a string
address string // full network address
pingTimer *time.Timer // we should send a PING
timeoutTimer *time.Timer // connection seems to be dead
registered bool // the user has registered
capNegotiating bool // negotiating capabilities
capsEnabled uint // enabled capabilities
capVersion uint // CAP protocol version
tlsCertFingerprint string // client certificate fingerprint
nickname string // IRC nickname (main identifier)
username string // IRC username
realname string // IRC realname (or e-mail)
mode uint // user's mode
awayMessage string // away message
lastActive int64 // last PRIVMSG, to get idle time
invites map[string]bool // channel invitations by operators
antiflood floodDetector // flood detector
}
// --- Channels ----------------------------------------------------------------
const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
const (
ircChanModeInviteOnly = 1 << iota
ircChanModeModerated
ircChanModeNoOutsideMsgs
ircChanModeQuiet
ircChanModePrivate
ircChanModeSecret
ircChanModeProtectedTopic
ircChanModeOperator
ircChanModeVoice
)
type channel struct {
name string // channel name
modes uint // channel modes
key string // channel key
userLimit int // user limit or -1
created int64 // creation time
topic string // channel topic
topicWho string // who set the topic
topicTime int64 // when the topic was set
userModes map[*client]uint // modes for all channel users
banList []string // ban list
exceptionList []string // exceptions from bans
inviteList []string // exceptions from +I
}
func newChannel() *channel {
return &channel{userLimit: -1}
}
// TODO: Port struct channel methods.
// --- IRC server context ------------------------------------------------------
type whowasInfo struct {
nickname, username, realname, hostname string
}
func newWhowasInfo(c *client) *whowasInfo {
return &whowasInfo{
nickname: c.nickname,
username: c.username,
realname: c.realname,
hostname: c.hostname,
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
type ircCommand struct {
name string
requiresRegistration bool
handler func(*message, *client)
nReceived uint // number of commands received
bytesReceived uint // number of bytes received total
}
type preparedEvent struct {
client *client
host string // client's hostname or literal IP address
isTLS bool // the client seems to use TLS
}
type readEvent struct {
client *client
data []byte // new data from the client
err error // read error
}
type writeEvent struct {
client *client
written int // amount of bytes written
err error // write error
}
// TODO: Port server_context. Maybe we want to keep it in a struct?
// XXX: Beware that maps with identifier keys need to be indexed correctly.
// We might want to enforce accessor functions for users and channels.
var (
started int64 // when has the server been started
users map[string]*client // maps nicknames to clients
channels map[string]*channel // maps channel names to data
handlers map[string]bool // TODO message handlers
capHandlers map[string]bool // TODO CAP message handlers
whowas map[string]*whowasInfo // WHOWAS registry
serverName string // our server name
pingInterval uint // ping interval in seconds
maxConnections int // max connections allowed or 0
motd []string // MOTD (none if empty)
operators map[string]bool // TLS certificate fingerprints for IRCops
)
var (
sigs = make(chan os.Signal, 1)
conns = make(chan net.Conn)
prepared = make(chan preparedEvent)
reads = make(chan readEvent)
writes = make(chan writeEvent)
timeouts = make(chan *client)
tlsConf *tls.Config
clients = make(map[*client]bool)
listener net.Listener
// TODO: quitting, quitTimer as they are named in kike?
inShutdown bool
shutdownTimer <-chan time.Time
)
// Forcefully tear down all connections.
func forceShutdown(reason string) {
if !inShutdown {
log.Fatalln("forceShutdown called without initiateShutdown")
}
log.Printf("forced shutdown (%s)\n", reason)
for c := range clients {
c.destroy("TODO")
}
}
// Initiate a clean shutdown of the whole daemon.
func initiateShutdown() {
log.Println("shutting down")
if err := listener.Close(); err != nil {
log.Println(err)
}
for c := range clients {
c.closeLink("TODO")
}
shutdownTimer = time.After(5 * time.Second)
inShutdown = true
}
// TODO: ircChannelCreate
func ircChannelDestroyIfEmpty(ch *channel) {
// TODO
}
// TODO: ircSendToRoommates
// Broadcast to all /other/ clients (telnet-friendly, also in accordance to
// the plan of extending this to an IRCd).
func broadcast(line string, except *client) {
for c := range clients {
if c != except {
c.send(line)
}
}
}
func ircSendToRoommates(c *client, message string) {
// TODO
}
// --- Clients (continued) -----------------------------------------------------
// TODO: Perhaps we should append to *[]byte for performance.
func clientModeToString(m uint, mode *string) {
if 0 != m&ircUserModeInvisible {
*mode += "i"
}
if 0 != m&ircUserModeRxWallops {
*mode += "w"
}
if 0 != m&ircUserModeRestricted {
*mode += "r"
}
if 0 != m&ircUserModeOperator {
*mode += "o"
}
if 0 != m&ircUserModeRxServerNotices {
*mode += "s"
}
}
func (c *client) getMode() string {
mode := ""
if c.awayMessage != "" {
mode += "a"
}
clientModeToString(c.mode, &mode)
return mode
}
func (c *client) send(line string) {
if c.conn == nil || c.closing {
return
}
// TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named.
oldOutQ := len(c.outQ)
// So far there's only one message tag we use, so we can do it simple;
// note that a 1024-character limit applies to messages with tags on.
if 0 != c.capsEnabled&ircCapServerTime {
c.outQ = time.Now().UTC().
AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ")
}
bytes := []byte(line)
if len(bytes) > ircMaxMessageLength {
bytes = bytes[:ircMaxMessageLength]
}
// TODO: Kill the connection above some "SendQ" threshold (careful!)
c.outQ = append(c.outQ, bytes...)
c.outQ = append(c.outQ, "\r\n"...)
c.flushOutQ()
// Technically we haven't sent it yet but that's a minor detail
c.nSentMessages++
c.sentBytes += len(c.outQ) - oldOutQ
}
func (c *client) sendf(format string, a ...interface{}) {
c.send(fmt.Sprintf(format, a...))
}
func (c *client) addToWhowas() {
// Only keeping one entry for each nickname.
// TODO: Make sure this list doesn't get too long, for example by
// putting them in a linked list ordered by time.
whowas[ircStrxfrm(c.nickname)] = newWhowasInfo(c)
}
func (c *client) unregister(reason string) {
if !c.registered {
return
}
ircSendToRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
c.nickname, c.username, c.hostname, reason))
// The eventual QUIT message will take care of state at clients.
for _, ch := range channels {
delete(ch.userModes, c)
ircChannelDestroyIfEmpty(ch)
}
c.addToWhowas()
delete(users, ircStrxfrm(c.nickname))
c.nickname = ""
c.registered = false
}
// TODO: Rename to kill.
// Close the connection and forget about the client.
func (c *client) destroy(reason string) {
if reason == "" {
reason = "Client exited"
}
c.unregister(reason)
// TODO: Log the address; seems like we always have c.address.
log.Println("client destroyed")
// Try to send a "close notify" alert if the TLS object is ready,
// otherwise just tear down the transport.
if c.conn != nil {
_ = c.conn.Close()
} else {
_ = c.transport.Close()
}
// Clean up the goroutine, although a spurious event may still be sent.
// TODO: Other timers if needed.
if c.killTimer != nil {
c.killTimer.Stop()
}
delete(clients, c)
}
// Tear down the client connection, trying to do so in a graceful manner.
func (c *client) closeLink(reason string) {
// Let's just cut the connection, the client can try again later.
// We also want to avoid accidentally writing to the socket before
// address resolution has finished.
if c.conn == nil {
c.destroy(reason)
return
}
if c.closing {
return
}
nickname := c.nickname
if nickname == "" {
nickname = "*"
}
// We push an "ERROR" message to the write buffer and let the writer send
// it, with some arbitrary timeout. The "closing" state makes sure
// that a/ we ignore any successive messages, and b/ that the connection
// is killed after the write buffer is transferred and emptied.
// (Since we send this message, we don't need to call CloseWrite here.)
c.sendf("ERROR :Closing link: %s[%s] (%s)",
nickname, c.hostname /* TODO host IP? */, reason)
c.closing = true
c.unregister(reason)
c.killTimer = time.AfterFunc(3*time.Second, func() {
timeouts <- c
})
}
func (c *client) inMaskList(masks []string) bool {
client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
for _, mask := range masks {
if ircFnmatch(mask, client) {
return true
}
}
return false
}
func (c *client) getTLSCertFingerprint() string {
if c.tls == nil {
return ""
}
peerCerts := c.tls.ConnectionState().PeerCertificates
if len(peerCerts) == 0 {
return ""
}
hash := sha256.Sum256(peerCerts[0].Raw)
return hex.EncodeToString(hash[:])
}
// --- Timers ------------------------------------------------------------------
// TODO
// --- IRC command handling ----------------------------------------------------
// XXX: ap doesn't really need to be a slice.
func (c *client) makeReply(id int, ap []interface{}) string {
nickname := c.nickname
if nickname == "" {
nickname = "*"
}
s := fmt.Sprintf(":%s %03d %s ", serverName, id, nickname)
a := fmt.Sprintf(defaultReplies[id], ap...)
return s + a
}
// XXX: This way we cannot typecheck the arguments, so we must be careful.
func (c *client) sendReply(id int, args ...interface{}) {
c.send(c.makeReply(id, args))
}
/// Send a space-separated list of words across as many replies as needed.
func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
common := c.makeReply(id, args)
// We always send at least one message (there might be a client that
// expects us to send this message at least once).
for len(items) > 0 {
// If not even a single item fits in the limit (which may happen,
// in theory) it just gets cropped. We could also skip it.
reply := append([]byte(common), items[0]...)
// Append as many items as fits in a single message.
for len(items) > 0 &&
len(reply)+1+len(items[0]) <= ircMaxMessageLength {
reply = append(reply, ' ')
reply = append(reply, items[0]...)
items = items[1:]
}
c.send(string(reply))
}
}
func (c *client) sendMOTD() {
if len(motd) == 0 {
c.sendReply(ERR_NOMOTD)
return
}
c.sendReply(RPL_MOTDSTART, serverName)
for _, line := range motd {
c.sendReply(RPL_MOTD, line)
}
c.sendReply(RPL_ENDOFMOTD)
}
func (c *client) sendLUSERS() {
nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
for c := range clients {
if c.registered {
nUsers++
} else {
nUnknown++
}
if 0 != c.mode&ircUserModeOperator {
nOpers++
}
}
nChannels := 0
for _, ch := range channels {
if 0 != ch.modes&ircChanModeSecret {
nChannels++
}
}
c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
if nOpers != 0 {
c.sendReply(RPL_LUSEROP, nOpers)
}
if nUnknown != 0 {
c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
}
if nChannels != 0 {
c.sendReply(RPL_LUSERCHANNELS, nChannels)
}
c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
}
func isThisMe(target string) bool {
// Target servers can also be matched by their users
if ircFnmatch(target, serverName) {
return true
}
_, ok := users[ircStrxfrm(target)]
return ok
}
func (c *client) sendISUPPORT() {
// Only # channels, +e supported, +I supported, unlimited arguments to MODE
c.sendReply(RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"+
" TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
" NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName)
}
func (c *client) tryFinishRegistration() {
// TODO: Check if the realname is really required.
if c.nickname == "" || c.username == "" || c.realname == "" {
return
}
if c.registered || c.capNegotiating {
return
}
c.registered = true
c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
c.sendReply(RPL_YOURHOST, serverName, "TODO version")
// The purpose of this message eludes me.
c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006"))
c.sendReply(RPL_MYINFO, serverName, "TODO version",
ircSupportedUserModes, ircSupportedChanModes)
c.sendISUPPORT()
c.sendLUSERS()
c.sendMOTD()
if mode := c.getMode(); mode != "" {
c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
}
c.tlsCertFingerprint = c.getTLSCertFingerprint()
if c.tlsCertFingerprint != "" {
c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
serverName, c.nickname, c.tlsCertFingerprint)
}
delete(whowas, ircStrxfrm(c.nickname))
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// IRCv3 capability negotiation. See http://ircv3.org for details.
type ircCapArgs struct {
subcommand string // the subcommand being processed
fullParams string // whole parameter string
params []string // split parameters
target string // target parameter for replies
}
var ircCapTable = []struct {
flag uint // flag
name string // name of the capability
}{
{ircCapMultiPrefix, "multi-prefix"},
{ircCapInviteNotify, "invite-notify"},
{ircCapEchoMessage, "echo-message"},
{ircCapUserhostInNames, "userhost-in-names"},
{ircCapServerTime, "server-time"},
}
func (c *client) handleCAPLS(a *ircCapArgs) {
if len(a.params) == 1 {
if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
"Ignoring invalid protocol version number")
} else {
c.capVersion = uint(ver)
}
}
c.capNegotiating = true
c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
" userhost-in-names server-time", serverName, a.target)
}
func (c *client) handleCAPLIST(a *ircCapArgs) {
caps := []string{}
for _, cap := range ircCapTable {
if 0 != c.capsEnabled&cap.flag {
caps = append(caps, cap.name)
}
}
c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
strings.Join(caps, " "))
}
func ircDecodeCapability(name string) uint {
for _, cap := range ircCapTable {
if cap.name == name {
return cap.flag
}
}
return 0
}
func (c *client) handleCAPREQ(a *ircCapArgs) {
c.capNegotiating = true
newCaps := c.capsEnabled
ok := true
for _, param := range a.params {
removing := false
name := param
if name[:1] == "-" {
removing = true
name = name[1:]
}
if cap := ircDecodeCapability(name); cap == 0 {
ok = false
} else if removing {
newCaps &= ^cap
} else {
newCaps |= cap
}
}
if ok {
c.capsEnabled = newCaps
c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
} else {
c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
}
}
func (c *client) handleCAPACK(a *ircCapArgs) {
if len(a.params) > 0 {
c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
"No acknowledgable capabilities supported")
}
}
func (c *client) handleCAPEND(a *ircCapArgs) {
c.capNegotiating = false
c.tryFinishRegistration()
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// TODO: Beware of case sensitivity, probably need to index it by ircStrxfrm,
// which should arguably be named ircToLower and ircToUpper or something.
var ircCapHandlers = map[string]func(*client, *ircCapArgs){
"LS": (*client).handleCAPLS,
"LIST": (*client).handleCAPLIST,
"REQ": (*client).handleCAPREQ,
"ACK": (*client).handleCAPACK,
"END": (*client).handleCAPEND,
}
// XXX: Maybe these also deserve to be methods for client? They operato on
// global state, though.
func ircHandleCAP(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
// TODO: This really does seem to warrant a method.
nickname := c.nickname
if nickname == "" {
nickname = "*"
}
args := &ircCapArgs{
target: nickname,
subcommand: msg.params[0],
fullParams: "",
params: []string{},
}
if len(msg.params) > 1 {
args.fullParams = msg.params[1]
// TODO: ignore_empty, likely create SplitSkipEmpty
args.params = strings.Split(args.fullParams, " ")
}
// FIXME: We should ASCII ToUpper the subcommand.
if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok {
c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
"Invalid CAP subcommand")
} else {
fn(c, args)
}
}
func ircHandlePASS(msg *message, c *client) {
if c.registered {
c.sendReply(ERR_ALREADYREGISTERED)
} else if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
}
// We have TLS client certificates for this purpose; ignoring.
}
func ircHandleNICK(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NONICKNAMEGIVEN)
return
}
nickname := msg.params[0]
if !ircIsValidNickname(nickname) {
c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
return
}
nicknameNormalized := ircStrxfrm(nickname)
if client, ok := users[nicknameNormalized]; ok && client != c {
c.sendReply(ERR_NICKNAMEINUSE, nickname)
return
}
if c.registered {
c.addToWhowas()
message := fmt.Sprintf(":%s!%s@%s NICK :%s",
c.nickname, c.username, c.hostname, nickname)
ircSendToRoommates(c, message)
c.send(message)
}
// Release the old nickname and allocate a new one.
if c.nickname != "" {
delete(users, ircStrxfrm(c.nickname))
}
c.nickname = nickname
users[nicknameNormalized] = c
c.tryFinishRegistration()
}
func ircHandleUSER(msg *message, c *client) {
if c.registered {
c.sendReply(ERR_ALREADYREGISTERED)
return
}
if len(msg.params) < 4 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
// Unfortunately the protocol doesn't give us any means of rejecting it
if !ircIsValidUsername(username) {
username = "*"
}
c.username = username
c.realname = realname
c.mode = 0
if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
if 0 != m&4 {
c.mode |= ircUserModeRxWallops
}
if 0 != m&8 {
c.mode |= ircUserModeInvisible
}
}
c.tryFinishRegistration()
}
func ircHandleUSERHOST(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
// TODO
}
func ircHandleLUSERS(msg *message, c *client) {
if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
return
}
c.sendLUSERS()
}
func ircHandleMOTD(msg *message, c *client) {
if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
return
}
c.sendMOTD()
}
func ircHandlePING(msg *message, c *client) {
// XXX: The RFC is pretty incomprehensible about the exact usage.
if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
} else if len(msg.params) < 1 {
c.sendReply(ERR_NOORIGIN)
} else {
c.sendf(":%s PONG :%s", serverName, msg.params[0])
}
}
func ircHandlePONG(msg *message, c *client) {
// We are the only server, so we don't have to care too much.
if len(msg.params) < 1 {
c.sendReply(ERR_NOORIGIN)
return
}
// Set a new timer to send another PING
// TODO
}
func ircHandleQUIT(msg *message, c *client) {
reason := c.nickname
if len(msg.params) > 0 {
reason = msg.params[0]
}
c.closeLink("Quit: " + reason)
}
func ircHandleTIME(msg *message, c *client) {
if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
return
}
c.sendReply(RPL_TIME, serverName,
time.Now().Format("Mon Jan _2 2006 15:04:05"))
}
func ircHandleVERSION(msg *message, c *client) {
if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
return
}
postVersion := 0
if debugMode {
postVersion = 1
}
c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName,
"TODO program name"+" "+"TODO version")
c.sendISUPPORT()
}
/*
func ircChannelMulticast(ch *channel, msg string, except *client) {
for c, m := range ch.userModes {
if c != except {
c.send(msg)
}
}
}
func ircModifyMode(mask *uint, mode uint, add bool) bool {
orig := *mask
if add {
*mask |= mode
} else {
*mask &= ^mode
}
return *mask != orig
}
func ircUpdateUserMode(c *client, newMode uint) {
// TODO: Port, as well as all the other kike functions.
}
*/
func ircHandleMODE(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
// TODO
target := msg.params[0]
client := users[ircStrxfrm(target)]
ch := users[ircStrxfrm(target)]
if client != nil {
// TODO: Think about strcmp.
//if ircStrcmp(target, c.nickname) != 0 {
//}
} else if ch != nil {
// TODO
}
}
func ircHandleUserMessage(msg *message, c *client,
command string, allowAwayReply bool) {
if len(msg.params) < 1 {
c.sendReply(ERR_NORECIPIENT, msg.command)
return
}
if len(msg.params) < 2 || msg.params[1] == "" {
c.sendReply(ERR_NOTEXTTOSEND)
return
}
target, text := msg.params[0], msg.params[1]
if client, ok := users[ircStrxfrm(target)]; ok {
// TODO
_ = client
_ = text
} else if ch, ok := channels[ircStrxfrm(target)]; ok {
// TODO
_ = ch
} else {
c.sendReply(ERR_NOSUCHNICK, target)
}
}
// TODO: All the various real command handlers.
func ircHandleX(msg *message, c *client) {
}
// --- ? -----------------------------------------------------------------------
// Handle the results from initializing the client's connection.
func (c *client) onPrepared(host string, isTLS bool) {
if isTLS {
c.tls = tls.Server(c.transport, tlsConf)
c.conn = c.tls
} else {
c.conn = c.transport.(connCloseWrite)
}
c.hostname = host
c.address = net.JoinHostPort(host, c.port)
// TODO: If we've tried to send any data before now, we need to flushOutQ.
go read(c)
c.reading = true
}
// Handle the results from trying to read from the client connection.
func (c *client) onRead(data []byte, readErr error) {
if !c.reading {
// Abusing the flag to emulate CloseRead and skip over data, see below.
return
}
c.inQ = append(c.inQ, data...)
for {
// XXX: This accepts even simple LF newlines, even though they're not
// really allowed by the protocol.
advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */)
if advance == 0 {
break
}
c.inQ = c.inQ[advance:]
line := string(token)
log.Printf("-> %s\n", line)
m := reMsg.FindStringSubmatch(line)
if m == nil {
log.Println("error: invalid line")
continue
}
msg := message{m[1], m[2], m[3], m[4], nil}
for _, x := range reArgs.FindAllString(m[5], -1) {
msg.params = append(msg.params, x[1:])
}
broadcast(line, c)
}
if readErr != nil {
c.reading = false
if readErr != io.EOF {
log.Println(readErr)
c.destroy("TODO")
} else if c.closing {
// Disregarding whether a clean shutdown has happened or not.
log.Println("client finished shutdown")
c.destroy("TODO")
} else {
log.Println("client EOF")
c.closeLink("")
}
} else if len(c.inQ) > 8192 {
log.Println("client inQ overrun")
c.closeLink("inQ overrun")
// tls.Conn doesn't have the CloseRead method (and it needs to be able
// to read from the TCP connection even for writes, so there isn't much
// sense in expecting the implementation to do anything useful),
// otherwise we'd use it to block incoming packet data.
c.reading = false
}
}
// Spawn a goroutine to flush the outQ if possible and necessary.
func (c *client) flushOutQ() {
if !c.writing && c.conn != nil {
go write(c, c.outQ)
c.writing = true
}
}
// Handle the results from trying to write to the client connection.
func (c *client) onWrite(written int, writeErr error) {
c.outQ = c.outQ[written:]
c.writing = false
if writeErr != nil {
log.Println(writeErr)
c.destroy("TODO")
} else if len(c.outQ) > 0 {
c.flushOutQ()
} else if c.closing {
if c.reading {
c.conn.CloseWrite()
} else {
c.destroy("TODO")
}
}
}
// --- Worker goroutines -------------------------------------------------------
func accept(ln net.Listener) {
for {
if conn, err := ln.Accept(); err != nil {
// TODO: Consider specific cases in error handling, some errors
// are transitional while others are fatal.
log.Println(err)
break
} else {
conns <- conn
}
}
}
func prepare(client *client) {
conn := client.transport
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
// In effect, we require TCP/UDP, as they have port numbers.
log.Fatalln(err)
}
// The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
// bothering with pointless contexts.
ch := make(chan string, 1)
go func() {
defer close(ch)
if names, err := net.LookupAddr(host); err != nil {
log.Println(err)
} else {
ch <- names[0]
}
}()
// While we can't cancel it, we still want to set a timeout on it.
select {
case <-time.After(5 * time.Second):
case resolved, ok := <-ch:
if ok {
host = resolved
}
}
// Note that in this demo application the autodetection prevents non-TLS
// clients from receiving any messages until they send something.
isTLS := false
if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
// This is just for the TLS detection and doesn't need to be fatal.
log.Println(err)
} else {
isTLS = detectTLS(sysconn)
}
// FIXME: When the client sends no data, we still initialize its conn.
prepared <- preparedEvent{client, host, isTLS}
}
func read(client *client) {
// A new buffer is allocated each time we receive some bytes, because of
// thread-safety. Therefore the buffer shouldn't be too large, or we'd
// need to copy it each time into a precisely sized new buffer.
var err error
for err == nil {
var (
buf [512]byte
n int
)
n, err = client.conn.Read(buf[:])
reads <- readEvent{client, buf[:n], err}
}
}
// Flush outQ, which is passed by parameter so that there are no data races.
func write(client *client, data []byte) {
// We just write as much as we can, the main goroutine does the looping.
n, err := client.conn.Write(data)
writes <- writeEvent{client, n, err}
}
// --- Main --------------------------------------------------------------------
func processOneEvent() {
select {
case <-sigs:
if inShutdown {
forceShutdown("requested by user")
} else {
initiateShutdown()
}
case <-shutdownTimer:
forceShutdown("timeout")
case conn := <-conns:
log.Println("accepted client connection")
// In effect, we require TCP/UDP, as they have port numbers.
address := conn.RemoteAddr().String()
host, port, err := net.SplitHostPort(address)
if err != nil {
log.Fatalln(err)
}
c := &client{
transport: conn,
address: address,
hostname: host,
port: port,
}
clients[c] = true
go prepare(c)
case ev := <-prepared:
log.Println("client is ready:", ev.host)
if _, ok := clients[ev.client]; ok {
ev.client.onPrepared(ev.host, ev.isTLS)
}
case ev := <-reads:
log.Println("received data from client")
if _, ok := clients[ev.client]; ok {
ev.client.onRead(ev.data, ev.err)
}
case ev := <-writes:
log.Println("sent data to client")
if _, ok := clients[ev.client]; ok {
ev.client.onWrite(ev.written, ev.err)
}
case c := <-timeouts:
if _, ok := clients[c]; ok {
log.Println("client timeouted")
c.destroy("TODO")
}
}
}
func main() {
flag.BoolVar(&debugMode, "debug", false, "debug mode")
version := flag.Bool("version", false, "show version and exit")
flag.Parse()
// TODO: Consider using the same version number for all subprojects.
if *version {
fmt.Printf("%s %s\n", "hid", "0")
return
}
// TODO: Configuration--create an INI parser, probably;
// lift XDG_CONFIG_HOME from gitlab-notifier.
if len(flag.Args()) != 3 {
log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
}
cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0))
if err != nil {
log.Fatalln(err)
}
tlsConf = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
}
listener, err = net.Listen("tcp", flag.Arg(2))
if err != nil {
log.Fatalln(err)
}
go accept(listener)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
for !inShutdown || len(clients) > 0 {
processOneEvent()
}
}