hid: first round of mixed fixes and cleanups
This commit is contained in:
		
							
								
								
									
										464
									
								
								hid/main.go
									
									
									
									
									
								
							
							
						
						
									
										464
									
								
								hid/main.go
									
									
									
									
									
								
							@@ -16,55 +16,101 @@
 | 
			
		||||
// hid is a straight-forward port of kike IRCd from C.
 | 
			
		||||
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
 | 
			
		||||
func tf(text string, ansi string) string {
 | 
			
		||||
	return "\x1b[0;" + ansi + "m" + text + "\x1b[0m"
 | 
			
		||||
}
 | 
			
		||||
var debugMode = false
 | 
			
		||||
 | 
			
		||||
func logErrorf(format string, args ...interface{}) {
 | 
			
		||||
	fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...)
 | 
			
		||||
}
 | 
			
		||||
const (
 | 
			
		||||
	projectName = "hid"
 | 
			
		||||
	// TODO: Consider using the same version number for all subprojects.
 | 
			
		||||
	projectVersion = "0"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func logFatalf(format string, args ...interface{}) {
 | 
			
		||||
	fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...)
 | 
			
		||||
	os.Exit(1)
 | 
			
		||||
}
 | 
			
		||||
// --- Utilities ---------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
 | 
			
		||||
func splitString(s, delims string, ignoreEmpty bool) (result []string) {
 | 
			
		||||
	for {
 | 
			
		||||
		end := strings.IndexAny(s, delims)
 | 
			
		||||
		if end < 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		if !ignoreEmpty || end != 0 {
 | 
			
		||||
			result = append(result, s[:end])
 | 
			
		||||
		}
 | 
			
		||||
		s = s[end+1:]
 | 
			
		||||
	}
 | 
			
		||||
	if !ignoreEmpty || s != "" {
 | 
			
		||||
		result = append(result, s)
 | 
			
		||||
	}
 | 
			
		||||
	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:]
 | 
			
		||||
func findTildeHome(username string) string {
 | 
			
		||||
	if username != "" {
 | 
			
		||||
		if u, _ := user.Lookup(username); u != nil {
 | 
			
		||||
			return u.HomeDir
 | 
			
		||||
		}
 | 
			
		||||
	} 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)
 | 
			
		||||
	if env != "" && env[0] == '/' {
 | 
			
		||||
		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
 | 
			
		||||
func getXdgConfigDirs() (result []string) {
 | 
			
		||||
	home := getXdgHomeDir("XDG_CONFIG_HOME", ".config")
 | 
			
		||||
// Retrieve all XDG base directories for configuration files.
 | 
			
		||||
func getXDGConfigDirs() (result []string) {
 | 
			
		||||
	home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
 | 
			
		||||
	if home != "" {
 | 
			
		||||
		result = append(result, home)
 | 
			
		||||
	}
 | 
			
		||||
@@ -80,56 +126,6 @@ func getXdgConfigDirs() (result []string) {
 | 
			
		||||
	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
 | 
			
		||||
@@ -173,6 +169,35 @@ var config = []struct {
 | 
			
		||||
	{"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 ------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
type floodDetector struct {
 | 
			
		||||
@@ -231,21 +256,40 @@ func ircToLower(c byte) byte {
 | 
			
		||||
	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 {
 | 
			
		||||
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, ircToLower(c))
 | 
			
		||||
		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 = ircStrxfrm(pattern), ircStrxfrm(s)
 | 
			
		||||
	// FIXME: This should not support [] ranges and handle / specially.
 | 
			
		||||
	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
 | 
			
		||||
@@ -331,8 +375,8 @@ 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
 | 
			
		||||
	recvQ     []byte         // unprocessed input
 | 
			
		||||
	sendQ     []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
 | 
			
		||||
@@ -459,10 +503,8 @@ type writeEvent struct {
 | 
			
		||||
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
 | 
			
		||||
	users    map[string]*client  // maps nicknames to clients
 | 
			
		||||
	channels map[string]*channel // maps channel names to data
 | 
			
		||||
 | 
			
		||||
	whowas map[string]*whowasInfo // WHOWAS registry
 | 
			
		||||
 | 
			
		||||
@@ -481,38 +523,38 @@ var (
 | 
			
		||||
	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
 | 
			
		||||
	tlsConf   *tls.Config
 | 
			
		||||
	clients   = make(map[*client]bool)
 | 
			
		||||
	listener  net.Listener
 | 
			
		||||
	quitting  bool
 | 
			
		||||
	quitTimer <-chan time.Time
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Forcefully tear down all connections.
 | 
			
		||||
func forceShutdown(reason string) {
 | 
			
		||||
	if !inShutdown {
 | 
			
		||||
		log.Fatalln("forceShutdown called without initiateShutdown")
 | 
			
		||||
func forceQuit(reason string) {
 | 
			
		||||
	if !quitting {
 | 
			
		||||
		log.Fatalln("forceQuit called without initiateQuit")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("forced shutdown (%s)\n", reason)
 | 
			
		||||
	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.
 | 
			
		||||
func initiateShutdown() {
 | 
			
		||||
func initiateQuit() {
 | 
			
		||||
	log.Println("shutting down")
 | 
			
		||||
	if err := listener.Close(); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
	for c := range clients {
 | 
			
		||||
		c.closeLink("TODO")
 | 
			
		||||
		c.closeLink("Shutting down")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shutdownTimer = time.After(5 * time.Second)
 | 
			
		||||
	inShutdown = true
 | 
			
		||||
	quitTimer = time.After(5 * time.Second)
 | 
			
		||||
	quitting = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: ircChannelCreate
 | 
			
		||||
@@ -538,32 +580,31 @@ func ircSendToRoommates(c *client, message string) {
 | 
			
		||||
 | 
			
		||||
// --- Clients (continued) -----------------------------------------------------
 | 
			
		||||
 | 
			
		||||
// TODO: Perhaps we should append to *[]byte for performance.
 | 
			
		||||
func clientModeToString(m uint, mode *string) {
 | 
			
		||||
func clientModeToString(m uint, mode *[]byte) {
 | 
			
		||||
	if 0 != m&ircUserModeInvisible {
 | 
			
		||||
		*mode += "i"
 | 
			
		||||
		*mode = append(*mode, 'i')
 | 
			
		||||
	}
 | 
			
		||||
	if 0 != m&ircUserModeRxWallops {
 | 
			
		||||
		*mode += "w"
 | 
			
		||||
		*mode = append(*mode, 'w')
 | 
			
		||||
	}
 | 
			
		||||
	if 0 != m&ircUserModeRestricted {
 | 
			
		||||
		*mode += "r"
 | 
			
		||||
		*mode = append(*mode, 'r')
 | 
			
		||||
	}
 | 
			
		||||
	if 0 != m&ircUserModeOperator {
 | 
			
		||||
		*mode += "o"
 | 
			
		||||
		*mode = append(*mode, 'o')
 | 
			
		||||
	}
 | 
			
		||||
	if 0 != m&ircUserModeRxServerNotices {
 | 
			
		||||
		*mode += "s"
 | 
			
		||||
		*mode = append(*mode, 's')
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) getMode() string {
 | 
			
		||||
	mode := ""
 | 
			
		||||
	var mode []byte
 | 
			
		||||
	if c.awayMessage != "" {
 | 
			
		||||
		mode += "a"
 | 
			
		||||
		mode = append(mode, 'a')
 | 
			
		||||
	}
 | 
			
		||||
	clientModeToString(c.mode, &mode)
 | 
			
		||||
	return mode
 | 
			
		||||
	return string(mode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) send(line string) {
 | 
			
		||||
@@ -571,14 +612,13 @@ func (c *client) send(line string) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named.
 | 
			
		||||
	oldOutQ := len(c.outQ)
 | 
			
		||||
	oldSendQLen := len(c.sendQ)
 | 
			
		||||
 | 
			
		||||
	// 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 ")
 | 
			
		||||
		c.sendQ = time.Now().UTC().
 | 
			
		||||
			AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bytes := []byte(line)
 | 
			
		||||
@@ -587,13 +627,13 @@ func (c *client) send(line string) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: Kill the connection above some "SendQ" threshold (careful!)
 | 
			
		||||
	c.outQ = append(c.outQ, bytes...)
 | 
			
		||||
	c.outQ = append(c.outQ, "\r\n"...)
 | 
			
		||||
	c.flushOutQ()
 | 
			
		||||
	c.sendQ = append(c.sendQ, bytes...)
 | 
			
		||||
	c.sendQ = append(c.sendQ, "\r\n"...)
 | 
			
		||||
	c.flushSendQ()
 | 
			
		||||
 | 
			
		||||
	// Technically we haven't sent it yet but that's a minor detail
 | 
			
		||||
	c.nSentMessages++
 | 
			
		||||
	c.sentBytes += len(c.outQ) - oldOutQ
 | 
			
		||||
	c.sentBytes += len(c.sendQ) - oldSendQLen
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) sendf(format string, a ...interface{}) {
 | 
			
		||||
@@ -604,7 +644,14 @@ 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)
 | 
			
		||||
	whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) nicknameOrStar() string {
 | 
			
		||||
	if c.nickname == "" {
 | 
			
		||||
		return "*"
 | 
			
		||||
	}
 | 
			
		||||
	return c.nickname
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) unregister(reason string) {
 | 
			
		||||
@@ -622,14 +669,13 @@ func (c *client) unregister(reason string) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.addToWhowas()
 | 
			
		||||
	delete(users, ircStrxfrm(c.nickname))
 | 
			
		||||
	delete(users, ircToCanon(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) {
 | 
			
		||||
func (c *client) kill(reason string) {
 | 
			
		||||
	if reason == "" {
 | 
			
		||||
		reason = "Client exited"
 | 
			
		||||
	}
 | 
			
		||||
@@ -661,25 +707,20 @@ func (c *client) closeLink(reason string) {
 | 
			
		||||
	// We also want to avoid accidentally writing to the socket before
 | 
			
		||||
	// address resolution has finished.
 | 
			
		||||
	if c.conn == nil {
 | 
			
		||||
		c.destroy(reason)
 | 
			
		||||
		c.kill(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.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
 | 
			
		||||
	c.closing = true
 | 
			
		||||
 | 
			
		||||
	c.unregister(reason)
 | 
			
		||||
@@ -720,12 +761,7 @@ func (c *client) getTLSCertFingerprint() string {
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
 | 
			
		||||
	a := fmt.Sprintf(defaultReplies[id], ap...)
 | 
			
		||||
	return s + a
 | 
			
		||||
}
 | 
			
		||||
@@ -809,7 +845,7 @@ func isThisMe(target string) bool {
 | 
			
		||||
	if ircFnmatch(target, serverName) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	_, ok := users[ircStrxfrm(target)]
 | 
			
		||||
	_, ok := users[ircToCanon(target)]
 | 
			
		||||
	return ok
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -821,21 +857,20 @@ func (c *client) sendISUPPORT() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *client) tryFinishRegistration() {
 | 
			
		||||
	// TODO: Check if the realname is really required.
 | 
			
		||||
	if c.nickname == "" || c.username == "" || c.realname == "" {
 | 
			
		||||
	if c.registered || c.capNegotiating {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if c.registered || c.capNegotiating {
 | 
			
		||||
	if c.nickname == "" || c.username == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.registered = true
 | 
			
		||||
	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.
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	c.sendISUPPORT()
 | 
			
		||||
@@ -852,7 +887,7 @@ func (c *client) tryFinishRegistration() {
 | 
			
		||||
			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){
 | 
			
		||||
	"LS":   (*client).handleCAPLS,
 | 
			
		||||
	"LIST": (*client).handleCAPLIST,
 | 
			
		||||
@@ -976,14 +1009,8 @@ func ircHandleCAP(msg *message, c *client) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: This really does seem to warrant a method.
 | 
			
		||||
	nickname := c.nickname
 | 
			
		||||
	if nickname == "" {
 | 
			
		||||
		nickname = "*"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args := &ircCapArgs{
 | 
			
		||||
		target:     nickname,
 | 
			
		||||
		target:     c.nicknameOrStar(),
 | 
			
		||||
		subcommand: msg.params[0],
 | 
			
		||||
		fullParams: "",
 | 
			
		||||
		params:     []string{},
 | 
			
		||||
@@ -991,12 +1018,10 @@ func ircHandleCAP(msg *message, c *client) {
 | 
			
		||||
 | 
			
		||||
	if len(msg.params) > 1 {
 | 
			
		||||
		args.fullParams = msg.params[1]
 | 
			
		||||
		// TODO: ignore_empty, likely create SplitSkipEmpty
 | 
			
		||||
		args.params = strings.Split(args.fullParams, " ")
 | 
			
		||||
		args.params = splitString(args.fullParams, " ", true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: We should ASCII ToUpper the subcommand.
 | 
			
		||||
	if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok {
 | 
			
		||||
	if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
 | 
			
		||||
		c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
 | 
			
		||||
			"Invalid CAP subcommand")
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -1026,8 +1051,8 @@ func ircHandleNICK(msg *message, c *client) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nicknameNormalized := ircStrxfrm(nickname)
 | 
			
		||||
	if client, ok := users[nicknameNormalized]; ok && client != c {
 | 
			
		||||
	nicknameCanon := ircToCanon(nickname)
 | 
			
		||||
	if client, ok := users[nicknameCanon]; ok && client != c {
 | 
			
		||||
		c.sendReply(ERR_NICKNAMEINUSE, nickname)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -1043,11 +1068,11 @@ func ircHandleNICK(msg *message, c *client) {
 | 
			
		||||
 | 
			
		||||
	// Release the old nickname and allocate a new one.
 | 
			
		||||
	if c.nickname != "" {
 | 
			
		||||
		delete(users, ircStrxfrm(c.nickname))
 | 
			
		||||
		delete(users, ircToCanon(c.nickname))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.nickname = nickname
 | 
			
		||||
	users[nicknameNormalized] = c
 | 
			
		||||
	users[nicknameCanon] = c
 | 
			
		||||
 | 
			
		||||
	c.tryFinishRegistration()
 | 
			
		||||
}
 | 
			
		||||
@@ -1064,7 +1089,7 @@ func ircHandleUSER(msg *message, c *client) {
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
		username = "*"
 | 
			
		||||
	}
 | 
			
		||||
@@ -1091,7 +1116,30 @@ func ircHandleUSERHOST(msg *message, c *client) {
 | 
			
		||||
		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) {
 | 
			
		||||
@@ -1162,8 +1210,8 @@ func ircHandleVERSION(msg *message, c *client) {
 | 
			
		||||
		postVersion = 1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName,
 | 
			
		||||
		"TODO program name"+" "+"TODO version")
 | 
			
		||||
	c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
 | 
			
		||||
		projectName+" "+projectVersion)
 | 
			
		||||
	c.sendISUPPORT()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1199,13 +1247,13 @@ func ircHandleMODE(msg *message, c *client) {
 | 
			
		||||
 | 
			
		||||
	// TODO
 | 
			
		||||
	target := msg.params[0]
 | 
			
		||||
	client := users[ircStrxfrm(target)]
 | 
			
		||||
	ch := users[ircStrxfrm(target)]
 | 
			
		||||
	client := users[ircToCanon(target)]
 | 
			
		||||
	ch := users[ircToCanon(target)]
 | 
			
		||||
 | 
			
		||||
	if client != nil {
 | 
			
		||||
		// TODO: Think about strcmp.
 | 
			
		||||
		//if ircStrcmp(target, c.nickname) != 0 {
 | 
			
		||||
		//}
 | 
			
		||||
		// TODO
 | 
			
		||||
		if ircEqual(target, c.nickname) {
 | 
			
		||||
		}
 | 
			
		||||
	} else if ch != nil {
 | 
			
		||||
		// TODO
 | 
			
		||||
	}
 | 
			
		||||
@@ -1223,11 +1271,11 @@ func ircHandleUserMessage(msg *message, c *client,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target, text := msg.params[0], msg.params[1]
 | 
			
		||||
	if client, ok := users[ircStrxfrm(target)]; ok {
 | 
			
		||||
	if client, ok := users[ircToCanon(target)]; ok {
 | 
			
		||||
		// TODO
 | 
			
		||||
		_ = client
 | 
			
		||||
		_ = text
 | 
			
		||||
	} else if ch, ok := channels[ircStrxfrm(target)]; ok {
 | 
			
		||||
	} else if ch, ok := channels[ircToCanon(target)]; ok {
 | 
			
		||||
		// TODO
 | 
			
		||||
		_ = ch
 | 
			
		||||
	} else {
 | 
			
		||||
@@ -1254,7 +1302,7 @@ func (c *client) onPrepared(host string, isTLS bool) {
 | 
			
		||||
	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.
 | 
			
		||||
	// TODO: If we've tried to send any data before now, we need to flushSendQ.
 | 
			
		||||
	go read(c)
 | 
			
		||||
	c.reading = true
 | 
			
		||||
}
 | 
			
		||||
@@ -1266,16 +1314,16 @@ func (c *client) onRead(data []byte, readErr error) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.inQ = append(c.inQ, data...)
 | 
			
		||||
	c.recvQ = append(c.recvQ, 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 */)
 | 
			
		||||
		advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
 | 
			
		||||
		if advance == 0 {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.inQ = c.inQ[advance:]
 | 
			
		||||
		c.recvQ = c.recvQ[advance:]
 | 
			
		||||
		line := string(token)
 | 
			
		||||
		log.Printf("-> %s\n", line)
 | 
			
		||||
 | 
			
		||||
@@ -1298,18 +1346,18 @@ func (c *client) onRead(data []byte, readErr error) {
 | 
			
		||||
 | 
			
		||||
		if readErr != io.EOF {
 | 
			
		||||
			log.Println(readErr)
 | 
			
		||||
			c.destroy("TODO")
 | 
			
		||||
			c.kill(readErr.Error())
 | 
			
		||||
		} else if c.closing {
 | 
			
		||||
			// Disregarding whether a clean shutdown has happened or not.
 | 
			
		||||
			log.Println("client finished shutdown")
 | 
			
		||||
			c.destroy("TODO")
 | 
			
		||||
			c.kill("TODO")
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Println("client EOF")
 | 
			
		||||
			c.closeLink("")
 | 
			
		||||
		}
 | 
			
		||||
	} else if len(c.inQ) > 8192 {
 | 
			
		||||
		log.Println("client inQ overrun")
 | 
			
		||||
		c.closeLink("inQ overrun")
 | 
			
		||||
	} else if len(c.recvQ) > 8192 {
 | 
			
		||||
		log.Println("client recvQ overrun")
 | 
			
		||||
		c.closeLink("recvQ 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
 | 
			
		||||
@@ -1319,29 +1367,29 @@ func (c *client) onRead(data []byte, readErr error) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Spawn a goroutine to flush the outQ if possible and necessary.
 | 
			
		||||
func (c *client) flushOutQ() {
 | 
			
		||||
// Spawn a goroutine to flush the sendQ if possible and necessary.
 | 
			
		||||
func (c *client) flushSendQ() {
 | 
			
		||||
	if !c.writing && c.conn != nil {
 | 
			
		||||
		go write(c, c.outQ)
 | 
			
		||||
		go write(c, c.sendQ)
 | 
			
		||||
		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.sendQ = c.sendQ[written:]
 | 
			
		||||
	c.writing = false
 | 
			
		||||
 | 
			
		||||
	if writeErr != nil {
 | 
			
		||||
		log.Println(writeErr)
 | 
			
		||||
		c.destroy("TODO")
 | 
			
		||||
	} else if len(c.outQ) > 0 {
 | 
			
		||||
		c.flushOutQ()
 | 
			
		||||
		c.kill(writeErr.Error())
 | 
			
		||||
	} else if len(c.sendQ) > 0 {
 | 
			
		||||
		c.flushSendQ()
 | 
			
		||||
	} else if c.closing {
 | 
			
		||||
		if c.reading {
 | 
			
		||||
			c.conn.CloseWrite()
 | 
			
		||||
		} 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) {
 | 
			
		||||
	// We just write as much as we can, the main goroutine does the looping.
 | 
			
		||||
	n, err := client.conn.Write(data)
 | 
			
		||||
@@ -1431,14 +1479,14 @@ func write(client *client, data []byte) {
 | 
			
		||||
func processOneEvent() {
 | 
			
		||||
	select {
 | 
			
		||||
	case <-sigs:
 | 
			
		||||
		if inShutdown {
 | 
			
		||||
			forceShutdown("requested by user")
 | 
			
		||||
		if quitting {
 | 
			
		||||
			forceQuit("requested by user")
 | 
			
		||||
		} else {
 | 
			
		||||
			initiateShutdown()
 | 
			
		||||
			initiateQuit()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	case <-shutdownTimer:
 | 
			
		||||
		forceShutdown("timeout")
 | 
			
		||||
	case <-quitTimer:
 | 
			
		||||
		forceQuit("timeout")
 | 
			
		||||
 | 
			
		||||
	case conn := <-conns:
 | 
			
		||||
		log.Println("accepted client connection")
 | 
			
		||||
@@ -1480,7 +1528,7 @@ func processOneEvent() {
 | 
			
		||||
	case c := <-timeouts:
 | 
			
		||||
		if _, ok := clients[c]; ok {
 | 
			
		||||
			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")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	// TODO: Consider using the same version number for all subprojects.
 | 
			
		||||
	if *version {
 | 
			
		||||
		fmt.Printf("%s %s\n", "hid", "0")
 | 
			
		||||
		fmt.Printf("%s %s\n", projectName, projectVersion)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: Configuration--create an INI parser, probably;
 | 
			
		||||
	//   lift XDG_CONFIG_HOME from gitlab-notifier.
 | 
			
		||||
	// TODO: Configuration--create an INI parser, probably.
 | 
			
		||||
	if len(flag.Args()) != 3 {
 | 
			
		||||
		log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
 | 
			
		||||
	}
 | 
			
		||||
@@ -1520,7 +1566,7 @@ func main() {
 | 
			
		||||
	go accept(listener)
 | 
			
		||||
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 | 
			
		||||
 | 
			
		||||
	for !inShutdown || len(clients) > 0 {
 | 
			
		||||
	for !quitting || len(clients) > 0 {
 | 
			
		||||
		processOneEvent()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user