hid: first round of mixed fixes and cleanups
This commit is contained in:
		
							parent
							
								
									2d287752d4
								
							
						
					
					
						commit
						208a8fcc7e
					
				
							
								
								
									
										454
									
								
								xS/main.go
									
									
									
									
									
								
							
							
						
						
									
										454
									
								
								xS/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:] |  | ||||||
| 		} | 		} | ||||||
| 	return path | 	} else if u, _ := user.Current(); u != nil { | ||||||
|  | 		return u.HomeDir | ||||||
|  | 	} else if v, ok := os.LookupEnv("HOME"); ok { | ||||||
|  | 		return v | ||||||
|  | 	} | ||||||
|  | 	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 | ||||||
| @ -461,8 +505,6 @@ var ( | |||||||
| 
 | 
 | ||||||
| 	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 | ||||||
| 
 | 
 | ||||||
| @ -484,35 +526,35 @@ var ( | |||||||
| 	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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user