hid: first round of mixed fixes and cleanups
This commit is contained in:
parent
f5def2e579
commit
2dfb4e45d1
464
hid/main.go
464
hid/main.go
@ -16,55 +16,101 @@
|
|||||||
// hid is a straight-forward port of kike IRCd from C.
|
// hid is a straight-forward port of kike IRCd from C.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
/*
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// ANSI terminal formatting, would be better if we had isatty() available
|
var debugMode = false
|
||||||
func tf(text string, ansi string) string {
|
|
||||||
return "\x1b[0;" + ansi + "m" + text + "\x1b[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
func logErrorf(format string, args ...interface{}) {
|
const (
|
||||||
fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...)
|
projectName = "hid"
|
||||||
}
|
// TODO: Consider using the same version number for all subprojects.
|
||||||
|
projectVersion = "0"
|
||||||
|
)
|
||||||
|
|
||||||
func logFatalf(format string, args ...interface{}) {
|
// --- Utilities ---------------------------------------------------------------
|
||||||
fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logFatal(object interface{}) {
|
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
|
||||||
logFatalf("%s", object)
|
func splitString(s, delims string, ignoreEmpty bool) (result []string) {
|
||||||
}
|
for {
|
||||||
|
end := strings.IndexAny(s, delims)
|
||||||
func getHome() (home string) {
|
if end < 0 {
|
||||||
if u, _ := user.Current(); u != nil {
|
break
|
||||||
home = u.HomeDir
|
}
|
||||||
} else {
|
if !ignoreEmpty || end != 0 {
|
||||||
home = os.Getenv("HOME")
|
result = append(result, s[:end])
|
||||||
|
}
|
||||||
|
s = s[end+1:]
|
||||||
|
}
|
||||||
|
if !ignoreEmpty || s != "" {
|
||||||
|
result = append(result, s)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only handling the simple case as that's what one mostly wants.
|
func findTildeHome(username string) string {
|
||||||
// TODO(p): Handle the generic case as well.
|
if username != "" {
|
||||||
func expandTilde(path string) string {
|
if u, _ := user.Lookup(username); u != nil {
|
||||||
if strings.HasPrefix(path, "~/") {
|
return u.HomeDir
|
||||||
return getHome() + path[1:]
|
}
|
||||||
|
} else if u, _ := user.Current(); u != nil {
|
||||||
|
return u.HomeDir
|
||||||
|
} else if v, ok := os.LookupEnv("HOME"); ok {
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
return path
|
return "~" + username
|
||||||
}
|
}
|
||||||
|
|
||||||
func getXdgHomeDir(name, def string) string {
|
// Tries to expand the tilde in paths, leaving it as-is on error.
|
||||||
|
func expandTilde(path string) string {
|
||||||
|
if path[0] != '~' {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for n = 0; n < len(path); n++ {
|
||||||
|
if path[n] == '/' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findTildeHome(path[1:n]) + path[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getXDGHomeDir(name, def string) string {
|
||||||
env := os.Getenv(name)
|
env := os.Getenv(name)
|
||||||
if env != "" && env[0] == '/' {
|
if env != "" && env[0] == '/' {
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
return filepath.Join(getHome(), def)
|
|
||||||
|
home := ""
|
||||||
|
if v, ok := os.LookupEnv("HOME"); ok {
|
||||||
|
home = v
|
||||||
|
} else if u, _ := user.Current(); u != nil {
|
||||||
|
home = u.HomeDir
|
||||||
|
}
|
||||||
|
return filepath.Join(home, def)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve all XDG base directories for configuration files
|
// Retrieve all XDG base directories for configuration files.
|
||||||
func getXdgConfigDirs() (result []string) {
|
func getXDGConfigDirs() (result []string) {
|
||||||
home := getXdgHomeDir("XDG_CONFIG_HOME", ".config")
|
home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
|
||||||
if home != "" {
|
if home != "" {
|
||||||
result = append(result, home)
|
result = append(result, home)
|
||||||
}
|
}
|
||||||
@ -80,56 +126,6 @@ func getXdgConfigDirs() (result []string) {
|
|||||||
return
|
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
|
// 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
|
// must be at least three octets long for this to work reliably, but that should
|
||||||
@ -173,6 +169,35 @@ var config = []struct {
|
|||||||
{"bind", []rune(":6667"), "Address of the IRC server"},
|
{"bind", []rune(":6667"), "Address of the IRC server"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// TODO: We don't want to use JSON.
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
// --- Rate limiter ------------------------------------------------------------
|
// --- Rate limiter ------------------------------------------------------------
|
||||||
|
|
||||||
type floodDetector struct {
|
type floodDetector struct {
|
||||||
@ -231,21 +256,40 @@ func ircToLower(c byte) byte {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: To support ALL CAPS initialization of maps, perhaps we should use
|
func ircToUpper(c byte) byte {
|
||||||
// ircToUpper instead.
|
switch c {
|
||||||
// FIXME: This doesn't follow the meaning of strxfrm and perhaps should be
|
case '{':
|
||||||
// renamed to ircNormalize.
|
return '['
|
||||||
func ircStrxfrm(ident string) string {
|
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
|
var canon []byte
|
||||||
for _, c := range []byte(ident) {
|
for _, c := range []byte(ident) {
|
||||||
canon = append(canon, ircToLower(c))
|
canon = append(canon, ircToUpper(c))
|
||||||
}
|
}
|
||||||
return string(canon)
|
return string(canon)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ircEqual(s1, s2 string) bool {
|
||||||
|
return ircToCanon(s1) == ircToCanon(s2)
|
||||||
|
}
|
||||||
|
|
||||||
func ircFnmatch(pattern string, s string) bool {
|
func ircFnmatch(pattern string, s string) bool {
|
||||||
pattern, s = ircStrxfrm(pattern), ircStrxfrm(s)
|
pattern, s = ircToCanon(pattern), ircToCanon(s)
|
||||||
// FIXME: This should not support [] ranges and handle / specially.
|
// FIXME: This should not support [] ranges and handle '/' specially.
|
||||||
// We could translate the pattern to a regular expression.
|
// We could translate the pattern to a regular expression.
|
||||||
matched, _ := filepath.Match(pattern, s)
|
matched, _ := filepath.Match(pattern, s)
|
||||||
return matched
|
return matched
|
||||||
@ -331,8 +375,8 @@ type client struct {
|
|||||||
transport net.Conn // underlying connection
|
transport net.Conn // underlying connection
|
||||||
tls *tls.Conn // TLS, if detected
|
tls *tls.Conn // TLS, if detected
|
||||||
conn connCloseWrite // high-level connection
|
conn connCloseWrite // high-level connection
|
||||||
inQ []byte // unprocessed input
|
recvQ []byte // unprocessed input
|
||||||
outQ []byte // unprocessed output
|
sendQ []byte // unprocessed output
|
||||||
reading bool // whether a reading goroutine is running
|
reading bool // whether a reading goroutine is running
|
||||||
writing bool // whether a writing goroutine is running
|
writing bool // whether a writing goroutine is running
|
||||||
closing bool // whether we're closing the connection
|
closing bool // whether we're closing the connection
|
||||||
@ -459,10 +503,8 @@ type writeEvent struct {
|
|||||||
var (
|
var (
|
||||||
started int64 // when has the server been started
|
started int64 // when has the server been started
|
||||||
|
|
||||||
users map[string]*client // maps nicknames to clients
|
users map[string]*client // maps nicknames to clients
|
||||||
channels map[string]*channel // maps channel names to data
|
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
|
whowas map[string]*whowasInfo // WHOWAS registry
|
||||||
|
|
||||||
@ -481,38 +523,38 @@ var (
|
|||||||
writes = make(chan writeEvent)
|
writes = make(chan writeEvent)
|
||||||
timeouts = make(chan *client)
|
timeouts = make(chan *client)
|
||||||
|
|
||||||
tlsConf *tls.Config
|
tlsConf *tls.Config
|
||||||
clients = make(map[*client]bool)
|
clients = make(map[*client]bool)
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
// TODO: quitting, quitTimer as they are named in kike?
|
quitting bool
|
||||||
inShutdown bool
|
quitTimer <-chan time.Time
|
||||||
shutdownTimer <-chan time.Time
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Forcefully tear down all connections.
|
// Forcefully tear down all connections.
|
||||||
func forceShutdown(reason string) {
|
func forceQuit(reason string) {
|
||||||
if !inShutdown {
|
if !quitting {
|
||||||
log.Fatalln("forceShutdown called without initiateShutdown")
|
log.Fatalln("forceQuit called without initiateQuit")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("forced shutdown (%s)\n", reason)
|
log.Printf("forced shutdown (%s)\n", reason)
|
||||||
for c := range clients {
|
for c := range clients {
|
||||||
c.destroy("TODO")
|
// initiateQuit has already unregistered the client.
|
||||||
|
c.kill("Shutting down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiate a clean shutdown of the whole daemon.
|
// Initiate a clean shutdown of the whole daemon.
|
||||||
func initiateShutdown() {
|
func initiateQuit() {
|
||||||
log.Println("shutting down")
|
log.Println("shutting down")
|
||||||
if err := listener.Close(); err != nil {
|
if err := listener.Close(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
for c := range clients {
|
for c := range clients {
|
||||||
c.closeLink("TODO")
|
c.closeLink("Shutting down")
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdownTimer = time.After(5 * time.Second)
|
quitTimer = time.After(5 * time.Second)
|
||||||
inShutdown = true
|
quitting = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ircChannelCreate
|
// TODO: ircChannelCreate
|
||||||
@ -538,32 +580,31 @@ func ircSendToRoommates(c *client, message string) {
|
|||||||
|
|
||||||
// --- Clients (continued) -----------------------------------------------------
|
// --- Clients (continued) -----------------------------------------------------
|
||||||
|
|
||||||
// TODO: Perhaps we should append to *[]byte for performance.
|
func clientModeToString(m uint, mode *[]byte) {
|
||||||
func clientModeToString(m uint, mode *string) {
|
|
||||||
if 0 != m&ircUserModeInvisible {
|
if 0 != m&ircUserModeInvisible {
|
||||||
*mode += "i"
|
*mode = append(*mode, 'i')
|
||||||
}
|
}
|
||||||
if 0 != m&ircUserModeRxWallops {
|
if 0 != m&ircUserModeRxWallops {
|
||||||
*mode += "w"
|
*mode = append(*mode, 'w')
|
||||||
}
|
}
|
||||||
if 0 != m&ircUserModeRestricted {
|
if 0 != m&ircUserModeRestricted {
|
||||||
*mode += "r"
|
*mode = append(*mode, 'r')
|
||||||
}
|
}
|
||||||
if 0 != m&ircUserModeOperator {
|
if 0 != m&ircUserModeOperator {
|
||||||
*mode += "o"
|
*mode = append(*mode, 'o')
|
||||||
}
|
}
|
||||||
if 0 != m&ircUserModeRxServerNotices {
|
if 0 != m&ircUserModeRxServerNotices {
|
||||||
*mode += "s"
|
*mode = append(*mode, 's')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) getMode() string {
|
func (c *client) getMode() string {
|
||||||
mode := ""
|
var mode []byte
|
||||||
if c.awayMessage != "" {
|
if c.awayMessage != "" {
|
||||||
mode += "a"
|
mode = append(mode, 'a')
|
||||||
}
|
}
|
||||||
clientModeToString(c.mode, &mode)
|
clientModeToString(c.mode, &mode)
|
||||||
return mode
|
return string(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) send(line string) {
|
func (c *client) send(line string) {
|
||||||
@ -571,14 +612,13 @@ func (c *client) send(line string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named.
|
oldSendQLen := len(c.sendQ)
|
||||||
oldOutQ := len(c.outQ)
|
|
||||||
|
|
||||||
// So far there's only one message tag we use, so we can do it simple;
|
// 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.
|
// note that a 1024-character limit applies to messages with tags on.
|
||||||
if 0 != c.capsEnabled&ircCapServerTime {
|
if 0 != c.capsEnabled&ircCapServerTime {
|
||||||
c.outQ = time.Now().UTC().
|
c.sendQ = time.Now().UTC().
|
||||||
AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ")
|
AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes := []byte(line)
|
bytes := []byte(line)
|
||||||
@ -587,13 +627,13 @@ func (c *client) send(line string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Kill the connection above some "SendQ" threshold (careful!)
|
// TODO: Kill the connection above some "SendQ" threshold (careful!)
|
||||||
c.outQ = append(c.outQ, bytes...)
|
c.sendQ = append(c.sendQ, bytes...)
|
||||||
c.outQ = append(c.outQ, "\r\n"...)
|
c.sendQ = append(c.sendQ, "\r\n"...)
|
||||||
c.flushOutQ()
|
c.flushSendQ()
|
||||||
|
|
||||||
// Technically we haven't sent it yet but that's a minor detail
|
// Technically we haven't sent it yet but that's a minor detail
|
||||||
c.nSentMessages++
|
c.nSentMessages++
|
||||||
c.sentBytes += len(c.outQ) - oldOutQ
|
c.sentBytes += len(c.sendQ) - oldSendQLen
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) sendf(format string, a ...interface{}) {
|
func (c *client) sendf(format string, a ...interface{}) {
|
||||||
@ -604,7 +644,14 @@ func (c *client) addToWhowas() {
|
|||||||
// Only keeping one entry for each nickname.
|
// Only keeping one entry for each nickname.
|
||||||
// TODO: Make sure this list doesn't get too long, for example by
|
// TODO: Make sure this list doesn't get too long, for example by
|
||||||
// putting them in a linked list ordered by time.
|
// putting them in a linked list ordered by time.
|
||||||
whowas[ircStrxfrm(c.nickname)] = newWhowasInfo(c)
|
whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) nicknameOrStar() string {
|
||||||
|
if c.nickname == "" {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
return c.nickname
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) unregister(reason string) {
|
func (c *client) unregister(reason string) {
|
||||||
@ -622,14 +669,13 @@ func (c *client) unregister(reason string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.addToWhowas()
|
c.addToWhowas()
|
||||||
delete(users, ircStrxfrm(c.nickname))
|
delete(users, ircToCanon(c.nickname))
|
||||||
c.nickname = ""
|
c.nickname = ""
|
||||||
c.registered = false
|
c.registered = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename to kill.
|
|
||||||
// Close the connection and forget about the client.
|
// Close the connection and forget about the client.
|
||||||
func (c *client) destroy(reason string) {
|
func (c *client) kill(reason string) {
|
||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "Client exited"
|
reason = "Client exited"
|
||||||
}
|
}
|
||||||
@ -661,25 +707,20 @@ func (c *client) closeLink(reason string) {
|
|||||||
// We also want to avoid accidentally writing to the socket before
|
// We also want to avoid accidentally writing to the socket before
|
||||||
// address resolution has finished.
|
// address resolution has finished.
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
c.destroy(reason)
|
c.kill(reason)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.closing {
|
if c.closing {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nickname := c.nickname
|
|
||||||
if nickname == "" {
|
|
||||||
nickname = "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
// We push an "ERROR" message to the write buffer and let the writer send
|
// We push an "ERROR" message to the write buffer and let the writer send
|
||||||
// it, with some arbitrary timeout. The "closing" state makes sure
|
// it, with some arbitrary timeout. The "closing" state makes sure
|
||||||
// that a/ we ignore any successive messages, and b/ that the connection
|
// that a/ we ignore any successive messages, and b/ that the connection
|
||||||
// is killed after the write buffer is transferred and emptied.
|
// is killed after the write buffer is transferred and emptied.
|
||||||
// (Since we send this message, we don't need to call CloseWrite here.)
|
// (Since we send this message, we don't need to call CloseWrite here.)
|
||||||
c.sendf("ERROR :Closing link: %s[%s] (%s)",
|
c.sendf("ERROR :Closing link: %s[%s] (%s)",
|
||||||
nickname, c.hostname /* TODO host IP? */, reason)
|
c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
|
||||||
c.closing = true
|
c.closing = true
|
||||||
|
|
||||||
c.unregister(reason)
|
c.unregister(reason)
|
||||||
@ -720,12 +761,7 @@ func (c *client) getTLSCertFingerprint() string {
|
|||||||
|
|
||||||
// XXX: ap doesn't really need to be a slice.
|
// XXX: ap doesn't really need to be a slice.
|
||||||
func (c *client) makeReply(id int, ap []interface{}) string {
|
func (c *client) makeReply(id int, ap []interface{}) string {
|
||||||
nickname := c.nickname
|
s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
|
||||||
if nickname == "" {
|
|
||||||
nickname = "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
s := fmt.Sprintf(":%s %03d %s ", serverName, id, nickname)
|
|
||||||
a := fmt.Sprintf(defaultReplies[id], ap...)
|
a := fmt.Sprintf(defaultReplies[id], ap...)
|
||||||
return s + a
|
return s + a
|
||||||
}
|
}
|
||||||
@ -809,7 +845,7 @@ func isThisMe(target string) bool {
|
|||||||
if ircFnmatch(target, serverName) {
|
if ircFnmatch(target, serverName) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
_, ok := users[ircStrxfrm(target)]
|
_, ok := users[ircToCanon(target)]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -821,21 +857,20 @@ func (c *client) sendISUPPORT() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) tryFinishRegistration() {
|
func (c *client) tryFinishRegistration() {
|
||||||
// TODO: Check if the realname is really required.
|
if c.registered || c.capNegotiating {
|
||||||
if c.nickname == "" || c.username == "" || c.realname == "" {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.registered || c.capNegotiating {
|
if c.nickname == "" || c.username == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.registered = true
|
c.registered = true
|
||||||
c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
|
c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
|
||||||
|
|
||||||
c.sendReply(RPL_YOURHOST, serverName, "TODO version")
|
c.sendReply(RPL_YOURHOST, serverName, projectVersion)
|
||||||
// The purpose of this message eludes me.
|
// The purpose of this message eludes me.
|
||||||
c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006"))
|
c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006"))
|
||||||
c.sendReply(RPL_MYINFO, serverName, "TODO version",
|
c.sendReply(RPL_MYINFO, serverName, projectVersion,
|
||||||
ircSupportedUserModes, ircSupportedChanModes)
|
ircSupportedUserModes, ircSupportedChanModes)
|
||||||
|
|
||||||
c.sendISUPPORT()
|
c.sendISUPPORT()
|
||||||
@ -852,7 +887,7 @@ func (c *client) tryFinishRegistration() {
|
|||||||
serverName, c.nickname, c.tlsCertFingerprint)
|
serverName, c.nickname, c.tlsCertFingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(whowas, ircStrxfrm(c.nickname))
|
delete(whowas, ircToCanon(c.nickname))
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
@ -957,8 +992,6 @@ func (c *client) handleCAPEND(a *ircCapArgs) {
|
|||||||
|
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
// 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){
|
var ircCapHandlers = map[string]func(*client, *ircCapArgs){
|
||||||
"LS": (*client).handleCAPLS,
|
"LS": (*client).handleCAPLS,
|
||||||
"LIST": (*client).handleCAPLIST,
|
"LIST": (*client).handleCAPLIST,
|
||||||
@ -976,14 +1009,8 @@ func ircHandleCAP(msg *message, c *client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This really does seem to warrant a method.
|
|
||||||
nickname := c.nickname
|
|
||||||
if nickname == "" {
|
|
||||||
nickname = "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
args := &ircCapArgs{
|
args := &ircCapArgs{
|
||||||
target: nickname,
|
target: c.nicknameOrStar(),
|
||||||
subcommand: msg.params[0],
|
subcommand: msg.params[0],
|
||||||
fullParams: "",
|
fullParams: "",
|
||||||
params: []string{},
|
params: []string{},
|
||||||
@ -991,12 +1018,10 @@ func ircHandleCAP(msg *message, c *client) {
|
|||||||
|
|
||||||
if len(msg.params) > 1 {
|
if len(msg.params) > 1 {
|
||||||
args.fullParams = msg.params[1]
|
args.fullParams = msg.params[1]
|
||||||
// TODO: ignore_empty, likely create SplitSkipEmpty
|
args.params = splitString(args.fullParams, " ", true)
|
||||||
args.params = strings.Split(args.fullParams, " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: We should ASCII ToUpper the subcommand.
|
if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
|
||||||
if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok {
|
|
||||||
c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
|
c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
|
||||||
"Invalid CAP subcommand")
|
"Invalid CAP subcommand")
|
||||||
} else {
|
} else {
|
||||||
@ -1026,8 +1051,8 @@ func ircHandleNICK(msg *message, c *client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nicknameNormalized := ircStrxfrm(nickname)
|
nicknameCanon := ircToCanon(nickname)
|
||||||
if client, ok := users[nicknameNormalized]; ok && client != c {
|
if client, ok := users[nicknameCanon]; ok && client != c {
|
||||||
c.sendReply(ERR_NICKNAMEINUSE, nickname)
|
c.sendReply(ERR_NICKNAMEINUSE, nickname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1043,11 +1068,11 @@ func ircHandleNICK(msg *message, c *client) {
|
|||||||
|
|
||||||
// Release the old nickname and allocate a new one.
|
// Release the old nickname and allocate a new one.
|
||||||
if c.nickname != "" {
|
if c.nickname != "" {
|
||||||
delete(users, ircStrxfrm(c.nickname))
|
delete(users, ircToCanon(c.nickname))
|
||||||
}
|
}
|
||||||
|
|
||||||
c.nickname = nickname
|
c.nickname = nickname
|
||||||
users[nicknameNormalized] = c
|
users[nicknameCanon] = c
|
||||||
|
|
||||||
c.tryFinishRegistration()
|
c.tryFinishRegistration()
|
||||||
}
|
}
|
||||||
@ -1064,7 +1089,7 @@ func ircHandleUSER(msg *message, c *client) {
|
|||||||
|
|
||||||
username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
|
username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
|
||||||
|
|
||||||
// Unfortunately the protocol doesn't give us any means of rejecting it
|
// Unfortunately, the protocol doesn't give us any means of rejecting it.
|
||||||
if !ircIsValidUsername(username) {
|
if !ircIsValidUsername(username) {
|
||||||
username = "*"
|
username = "*"
|
||||||
}
|
}
|
||||||
@ -1091,7 +1116,30 @@ func ircHandleUSERHOST(msg *message, c *client) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
var reply []byte
|
||||||
|
for i := 0; i < 5 && i < len(msg.params); i++ {
|
||||||
|
nick := msg.params[i]
|
||||||
|
target := users[ircToCanon(nick)]
|
||||||
|
if target == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i != 0 {
|
||||||
|
reply = append(reply, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
reply = append(reply, nick...)
|
||||||
|
if 0 != target.mode&ircUserModeOperator {
|
||||||
|
reply = append(reply, '*')
|
||||||
|
}
|
||||||
|
|
||||||
|
if target.awayMessage != "" {
|
||||||
|
reply = append(reply, "=-"...)
|
||||||
|
} else {
|
||||||
|
reply = append(reply, "=+"...)
|
||||||
|
}
|
||||||
|
reply = append(reply, (target.username + "@" + target.hostname)...)
|
||||||
|
}
|
||||||
|
c.sendReply(RPL_USERHOST, string(reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ircHandleLUSERS(msg *message, c *client) {
|
func ircHandleLUSERS(msg *message, c *client) {
|
||||||
@ -1162,8 +1210,8 @@ func ircHandleVERSION(msg *message, c *client) {
|
|||||||
postVersion = 1
|
postVersion = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName,
|
c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
|
||||||
"TODO program name"+" "+"TODO version")
|
projectName+" "+projectVersion)
|
||||||
c.sendISUPPORT()
|
c.sendISUPPORT()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1199,13 +1247,13 @@ func ircHandleMODE(msg *message, c *client) {
|
|||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
target := msg.params[0]
|
target := msg.params[0]
|
||||||
client := users[ircStrxfrm(target)]
|
client := users[ircToCanon(target)]
|
||||||
ch := users[ircStrxfrm(target)]
|
ch := users[ircToCanon(target)]
|
||||||
|
|
||||||
if client != nil {
|
if client != nil {
|
||||||
// TODO: Think about strcmp.
|
// TODO
|
||||||
//if ircStrcmp(target, c.nickname) != 0 {
|
if ircEqual(target, c.nickname) {
|
||||||
//}
|
}
|
||||||
} else if ch != nil {
|
} else if ch != nil {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
@ -1223,11 +1271,11 @@ func ircHandleUserMessage(msg *message, c *client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
target, text := msg.params[0], msg.params[1]
|
target, text := msg.params[0], msg.params[1]
|
||||||
if client, ok := users[ircStrxfrm(target)]; ok {
|
if client, ok := users[ircToCanon(target)]; ok {
|
||||||
// TODO
|
// TODO
|
||||||
_ = client
|
_ = client
|
||||||
_ = text
|
_ = text
|
||||||
} else if ch, ok := channels[ircStrxfrm(target)]; ok {
|
} else if ch, ok := channels[ircToCanon(target)]; ok {
|
||||||
// TODO
|
// TODO
|
||||||
_ = ch
|
_ = ch
|
||||||
} else {
|
} else {
|
||||||
@ -1254,7 +1302,7 @@ func (c *client) onPrepared(host string, isTLS bool) {
|
|||||||
c.hostname = host
|
c.hostname = host
|
||||||
c.address = net.JoinHostPort(host, c.port)
|
c.address = net.JoinHostPort(host, c.port)
|
||||||
|
|
||||||
// TODO: If we've tried to send any data before now, we need to flushOutQ.
|
// TODO: If we've tried to send any data before now, we need to flushSendQ.
|
||||||
go read(c)
|
go read(c)
|
||||||
c.reading = true
|
c.reading = true
|
||||||
}
|
}
|
||||||
@ -1266,16 +1314,16 @@ func (c *client) onRead(data []byte, readErr error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.inQ = append(c.inQ, data...)
|
c.recvQ = append(c.recvQ, data...)
|
||||||
for {
|
for {
|
||||||
// XXX: This accepts even simple LF newlines, even though they're not
|
// XXX: This accepts even simple LF newlines, even though they're not
|
||||||
// really allowed by the protocol.
|
// really allowed by the protocol.
|
||||||
advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */)
|
advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
|
||||||
if advance == 0 {
|
if advance == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
c.inQ = c.inQ[advance:]
|
c.recvQ = c.recvQ[advance:]
|
||||||
line := string(token)
|
line := string(token)
|
||||||
log.Printf("-> %s\n", line)
|
log.Printf("-> %s\n", line)
|
||||||
|
|
||||||
@ -1298,18 +1346,18 @@ func (c *client) onRead(data []byte, readErr error) {
|
|||||||
|
|
||||||
if readErr != io.EOF {
|
if readErr != io.EOF {
|
||||||
log.Println(readErr)
|
log.Println(readErr)
|
||||||
c.destroy("TODO")
|
c.kill(readErr.Error())
|
||||||
} else if c.closing {
|
} else if c.closing {
|
||||||
// Disregarding whether a clean shutdown has happened or not.
|
// Disregarding whether a clean shutdown has happened or not.
|
||||||
log.Println("client finished shutdown")
|
log.Println("client finished shutdown")
|
||||||
c.destroy("TODO")
|
c.kill("TODO")
|
||||||
} else {
|
} else {
|
||||||
log.Println("client EOF")
|
log.Println("client EOF")
|
||||||
c.closeLink("")
|
c.closeLink("")
|
||||||
}
|
}
|
||||||
} else if len(c.inQ) > 8192 {
|
} else if len(c.recvQ) > 8192 {
|
||||||
log.Println("client inQ overrun")
|
log.Println("client recvQ overrun")
|
||||||
c.closeLink("inQ overrun")
|
c.closeLink("recvQ overrun")
|
||||||
|
|
||||||
// tls.Conn doesn't have the CloseRead method (and it needs to be able
|
// 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
|
// to read from the TCP connection even for writes, so there isn't much
|
||||||
@ -1319,29 +1367,29 @@ func (c *client) onRead(data []byte, readErr error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn a goroutine to flush the outQ if possible and necessary.
|
// Spawn a goroutine to flush the sendQ if possible and necessary.
|
||||||
func (c *client) flushOutQ() {
|
func (c *client) flushSendQ() {
|
||||||
if !c.writing && c.conn != nil {
|
if !c.writing && c.conn != nil {
|
||||||
go write(c, c.outQ)
|
go write(c, c.sendQ)
|
||||||
c.writing = true
|
c.writing = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the results from trying to write to the client connection.
|
// Handle the results from trying to write to the client connection.
|
||||||
func (c *client) onWrite(written int, writeErr error) {
|
func (c *client) onWrite(written int, writeErr error) {
|
||||||
c.outQ = c.outQ[written:]
|
c.sendQ = c.sendQ[written:]
|
||||||
c.writing = false
|
c.writing = false
|
||||||
|
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
log.Println(writeErr)
|
log.Println(writeErr)
|
||||||
c.destroy("TODO")
|
c.kill(writeErr.Error())
|
||||||
} else if len(c.outQ) > 0 {
|
} else if len(c.sendQ) > 0 {
|
||||||
c.flushOutQ()
|
c.flushSendQ()
|
||||||
} else if c.closing {
|
} else if c.closing {
|
||||||
if c.reading {
|
if c.reading {
|
||||||
c.conn.CloseWrite()
|
c.conn.CloseWrite()
|
||||||
} else {
|
} else {
|
||||||
c.destroy("TODO")
|
c.kill("TODO")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1419,7 +1467,7 @@ func read(client *client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush outQ, which is passed by parameter so that there are no data races.
|
// Flush sendQ, which is passed by parameter so that there are no data races.
|
||||||
func write(client *client, data []byte) {
|
func write(client *client, data []byte) {
|
||||||
// We just write as much as we can, the main goroutine does the looping.
|
// We just write as much as we can, the main goroutine does the looping.
|
||||||
n, err := client.conn.Write(data)
|
n, err := client.conn.Write(data)
|
||||||
@ -1431,14 +1479,14 @@ func write(client *client, data []byte) {
|
|||||||
func processOneEvent() {
|
func processOneEvent() {
|
||||||
select {
|
select {
|
||||||
case <-sigs:
|
case <-sigs:
|
||||||
if inShutdown {
|
if quitting {
|
||||||
forceShutdown("requested by user")
|
forceQuit("requested by user")
|
||||||
} else {
|
} else {
|
||||||
initiateShutdown()
|
initiateQuit()
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-shutdownTimer:
|
case <-quitTimer:
|
||||||
forceShutdown("timeout")
|
forceQuit("timeout")
|
||||||
|
|
||||||
case conn := <-conns:
|
case conn := <-conns:
|
||||||
log.Println("accepted client connection")
|
log.Println("accepted client connection")
|
||||||
@ -1480,7 +1528,7 @@ func processOneEvent() {
|
|||||||
case c := <-timeouts:
|
case c := <-timeouts:
|
||||||
if _, ok := clients[c]; ok {
|
if _, ok := clients[c]; ok {
|
||||||
log.Println("client timeouted")
|
log.Println("client timeouted")
|
||||||
c.destroy("TODO")
|
c.kill("TODO")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1490,14 +1538,12 @@ func main() {
|
|||||||
version := flag.Bool("version", false, "show version and exit")
|
version := flag.Bool("version", false, "show version and exit")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// TODO: Consider using the same version number for all subprojects.
|
|
||||||
if *version {
|
if *version {
|
||||||
fmt.Printf("%s %s\n", "hid", "0")
|
fmt.Printf("%s %s\n", projectName, projectVersion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Configuration--create an INI parser, probably;
|
// TODO: Configuration--create an INI parser, probably.
|
||||||
// lift XDG_CONFIG_HOME from gitlab-notifier.
|
|
||||||
if len(flag.Args()) != 3 {
|
if len(flag.Args()) != 3 {
|
||||||
log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
|
log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
|
||||||
}
|
}
|
||||||
@ -1520,7 +1566,7 @@ func main() {
|
|||||||
go accept(listener)
|
go accept(listener)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
for !inShutdown || len(clients) > 0 {
|
for !quitting || len(clients) > 0 {
|
||||||
processOneEvent()
|
processOneEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user