hid: first round of mixed fixes and cleanups

This commit is contained in:
Přemysl Eric Janouch 2018-07-29 07:50:27 +02:00
parent f5def2e579
commit 2dfb4e45d1
Signed by: p
GPG Key ID: A0420B94F92B9493

View File

@ -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" const (
projectName = "hid"
// TODO: Consider using the same version number for all subprojects.
projectVersion = "0"
)
// --- Utilities ---------------------------------------------------------------
// 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 {
func logErrorf(format string, args ...interface{}) { result = append(result, s[:end])
fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...)
} }
s = s[end+1:]
func logFatalf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...)
os.Exit(1)
} }
if !ignoreEmpty || s != "" {
func logFatal(object interface{}) { result = append(result, s)
logFatalf("%s", object)
}
func getHome() (home string) {
if u, _ := user.Current(); u != nil {
home = u.HomeDir
} else {
home = os.Getenv("HOME")
} }
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 "~" + username
}
// Tries to expand the tilde in paths, leaving it as-is on error.
func expandTilde(path string) string {
if path[0] != '~' {
return path return path
} }
func getXdgHomeDir(name, def string) string { 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()
} }
} }