Compare commits
8 Commits
23f637dd47
...
f8bcfe447c
Author | SHA1 | Date | |
---|---|---|---|
f8bcfe447c | |||
fb648c37be | |||
bb0113021a | |||
5a40d7c2ed | |||
f32e2f1483 | |||
62418ebb54 | |||
6b45865e3d | |||
198cb87036 |
14
README
14
README
@ -108,7 +108,7 @@ The c2 wiki unsurprisingly has a lot of material around the design and
|
|||||||
realisation of GUIs, which might be useful.
|
realisation of GUIs, which might be useful.
|
||||||
|
|
||||||
It seems like an aligning/constraint-based "layout manager" will be one of the
|
It seems like an aligning/constraint-based "layout manager" will be one of the
|
||||||
first harder problems here. However I certainly don't want to used fixed
|
first harder problems here. However I certainly don't want to use fixed
|
||||||
coordinates as they would introduce problems with different fonts and i18n.
|
coordinates as they would introduce problems with different fonts and i18n.
|
||||||
|
|
||||||
We could use BDF fonts from the X11 distribution, but draw2d has native support
|
We could use BDF fonts from the X11 distribution, but draw2d has native support
|
||||||
@ -117,13 +117,18 @@ for FreeType fonts and it's more of a choice between vectors and bitmaps.
|
|||||||
The looks will be heavily inspired by Haiku and Windows 2000 and the user will
|
The looks will be heavily inspired by Haiku and Windows 2000 and the user will
|
||||||
have no say in this, for simplicity.
|
have no say in this, for simplicity.
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
- https://github.com/golang/exp/tree/master/shiny is a GUI library
|
||||||
|
- https://github.com/as/shiny is a fork of it
|
||||||
|
- http://man.cat-v.org/plan_9/1/rio has a particular, unusual model
|
||||||
|
|
||||||
Internationalisation
|
Internationalisation
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
For i18n https://github.com/leonelquinteros/gotext could be used, however I'll
|
For i18n https://github.com/leonelquinteros/gotext could be used, however I'll
|
||||||
probably give up on this issue as I'm fine enough with English.
|
probably give up on this issue as I'm fine enough with English.
|
||||||
|
|
||||||
Go also has x/text packages for this purpose, which might be better than GNU,
|
Go also has x/text packages for this purpose, which might be better than GNU,
|
||||||
but is essentially still in development.
|
but they're essentially still in development.
|
||||||
|
|
||||||
Versioning
|
Versioning
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
@ -191,7 +196,7 @@ takes with my 10 dictionaries isn't particularly bad), pack everything with
|
|||||||
archive/zip.
|
archive/zip.
|
||||||
|
|
||||||
Instead of ICU we may use x/text/collate and that's about everything we need.
|
Instead of ICU we may use x/text/collate and that's about everything we need.
|
||||||
Since we have our own format, we may expect the indexed to be ordered by the
|
Since we have our own format, we may expect the index to be ordered by the
|
||||||
locale's rules, assuming they don't change between versions.
|
locale's rules, assuming they don't change between versions.
|
||||||
|
|
||||||
hmpc -- MPD client
|
hmpc -- MPD client
|
||||||
@ -220,6 +225,9 @@ The real model for the editor is Qt Creator with FakeVIM, though this is not to
|
|||||||
be a clone of it, e.g. the various "Output" lists could be just special buffers,
|
be a clone of it, e.g. the various "Output" lists could be just special buffers,
|
||||||
which may be have names starting on "// ".
|
which may be have names starting on "// ".
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
- http://doc.cat-v.org/plan_9/4th_edition/papers/sam/
|
||||||
|
|
||||||
hfm -- file manager
|
hfm -- file manager
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
All we need to achieve here is replace Midnight Commander, which besides the
|
All we need to achieve here is replace Midnight Commander, which besides the
|
||||||
|
237
hid/main.go
237
hid/main.go
@ -27,7 +27,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log/syslog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@ -48,6 +48,100 @@ const (
|
|||||||
projectVersion = "0"
|
projectVersion = "0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// --- Logging -----------------------------------------------------------------
|
||||||
|
|
||||||
|
type logPrio int
|
||||||
|
|
||||||
|
const (
|
||||||
|
prioFatal logPrio = iota
|
||||||
|
prioError
|
||||||
|
prioWarning
|
||||||
|
prioStatus
|
||||||
|
prioDebug
|
||||||
|
)
|
||||||
|
|
||||||
|
func (lp logPrio) prefix() string {
|
||||||
|
switch lp {
|
||||||
|
case prioFatal:
|
||||||
|
return "fatal: "
|
||||||
|
case prioError:
|
||||||
|
return "error: "
|
||||||
|
case prioWarning:
|
||||||
|
return "warning: "
|
||||||
|
case prioStatus:
|
||||||
|
return ""
|
||||||
|
case prioDebug:
|
||||||
|
return "debug: "
|
||||||
|
default:
|
||||||
|
panic("unhandled log priority")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lp logPrio) syslogPrio() syslog.Priority {
|
||||||
|
switch lp {
|
||||||
|
case prioFatal:
|
||||||
|
return syslog.LOG_ERR
|
||||||
|
case prioError:
|
||||||
|
return syslog.LOG_ERR
|
||||||
|
case prioWarning:
|
||||||
|
return syslog.LOG_WARNING
|
||||||
|
case prioStatus:
|
||||||
|
return syslog.LOG_INFO
|
||||||
|
case prioDebug:
|
||||||
|
return syslog.LOG_DEBUG
|
||||||
|
default:
|
||||||
|
panic("unhandled log priority")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
func logMessageStdio(prio logPrio, format string, args ...interface{}) {
|
||||||
|
// TODO: isatty-enabled colors based on prio.
|
||||||
|
os.Stderr.WriteString(time.Now().Format("2006-01-02 15:04:05 ") +
|
||||||
|
prio.prefix() + fmt.Sprintf(format, args...) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func logMessageSystemd(prio logPrio, format string, args ...interface{}) {
|
||||||
|
if prio == prioFatal {
|
||||||
|
// There is no corresponding syslog severity.
|
||||||
|
format = "fatal: " + format
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "<%d>%s\n",
|
||||||
|
prio.syslogPrio(), fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
var logMessage = logMessageStdio
|
||||||
|
|
||||||
|
func printDebug(format string, args ...interface{}) {
|
||||||
|
if debugMode {
|
||||||
|
logMessage(prioDebug, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatus(format string, args ...interface{}) {
|
||||||
|
logMessage(prioStatus, format, args...)
|
||||||
|
}
|
||||||
|
func printWarning(format string, args ...interface{}) {
|
||||||
|
logMessage(prioWarning, format, args...)
|
||||||
|
}
|
||||||
|
func printError(format string, args ...interface{}) {
|
||||||
|
logMessage(prioError, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "fatal" is reserved for failures that would harm further operation.
|
||||||
|
|
||||||
|
func printFatal(format string, args ...interface{}) {
|
||||||
|
logMessage(prioFatal, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitFatal(format string, args ...interface{}) {
|
||||||
|
printFatal(format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Utilities ---------------------------------------------------------------
|
// --- Utilities ---------------------------------------------------------------
|
||||||
|
|
||||||
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
|
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
|
||||||
@ -164,9 +258,7 @@ func findTildeHome(username string) string {
|
|||||||
} else if v, ok := os.LookupEnv("HOME"); ok {
|
} else if v, ok := os.LookupEnv("HOME"); ok {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
if debugMode {
|
printDebug("failed to expand the home directory for %s", username)
|
||||||
log.Printf("failed to expand the home directory for %s", username)
|
|
||||||
}
|
|
||||||
return "~" + username
|
return "~" + username
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,27 +284,30 @@ func resolveFilename(filename string, relativeCB func(string) string) string {
|
|||||||
// --- Simple file I/O ---------------------------------------------------------
|
// --- Simple file I/O ---------------------------------------------------------
|
||||||
|
|
||||||
// Overwrites filename contents with data; creates directories as needed.
|
// Overwrites filename contents with data; creates directories as needed.
|
||||||
func writeFile(filename string, data []byte) error {
|
func writeFile(path string, data []byte) error {
|
||||||
if dir := filepath.Dir(filename); dir != "." {
|
if dir := filepath.Dir(path); dir != "." {
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(filename, data, 0644)
|
return ioutil.WriteFile(path, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper for writeFile that makes sure that the new data has been written
|
// Wrapper for writeFile that makes sure that the new data has been written
|
||||||
// to disk in its entirety before overriding the old file.
|
// to disk in its entirety before overriding the old file.
|
||||||
func writeFileSafe(filename string, data []byte) error {
|
func writeFileSafe(path string, data []byte) error {
|
||||||
temp := filename + ".new"
|
temp := path + ".new"
|
||||||
if err := writeFile(temp, data); err != nil {
|
if err := writeFile(temp, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.Rename(temp, filename)
|
return os.Rename(temp, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Simple configuration ----------------------------------------------------
|
// --- Simple configuration ----------------------------------------------------
|
||||||
|
|
||||||
|
// This is the bare minimum to make an application configurable.
|
||||||
|
// Keys are stripped of surrounding whitespace, values are not.
|
||||||
|
|
||||||
type simpleConfigItem struct {
|
type simpleConfigItem struct {
|
||||||
key string // INI key
|
key string // INI key
|
||||||
def string // default value
|
def string // default value
|
||||||
@ -229,8 +324,8 @@ func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
|
|||||||
|
|
||||||
func (sc simpleConfig) updateFromFile() error {
|
func (sc simpleConfig) updateFromFile() error {
|
||||||
basename := projectName + ".conf"
|
basename := projectName + ".conf"
|
||||||
filename := resolveFilename(basename, resolveRelativeConfigFilename)
|
path := resolveFilename(basename, resolveRelativeConfigFilename)
|
||||||
if filename == "" {
|
if path == "" {
|
||||||
return &os.PathError{
|
return &os.PathError{
|
||||||
Op: "cannot find",
|
Op: "cannot find",
|
||||||
Path: basename,
|
Path: basename,
|
||||||
@ -238,7 +333,7 @@ func (sc simpleConfig) updateFromFile() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Open(filename)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -253,7 +348,7 @@ func (sc simpleConfig) updateFromFile() error {
|
|||||||
|
|
||||||
equals := strings.IndexByte(line, '=')
|
equals := strings.IndexByte(line, '=')
|
||||||
if equals <= 0 {
|
if equals <= 0 {
|
||||||
return fmt.Errorf("%s:%d: malformed line", filename, lineNo)
|
return fmt.Errorf("%s:%d: malformed line", path, lineNo)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
|
sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
|
||||||
@ -295,13 +390,13 @@ func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
|
|||||||
``,
|
``,
|
||||||
}
|
}
|
||||||
|
|
||||||
filename, err := simpleConfigWriteDefault(
|
path, err := simpleConfigWriteDefault(
|
||||||
pathHint, strings.Join(prologLines, "\n"), table)
|
pathHint, strings.Join(prologLines, "\n"), table)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
exitFatal("%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("configuration written to `%s'\n", filename)
|
printStatus("configuration written to `%s'", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Configuration -----------------------------------------------------------
|
// --- Configuration -----------------------------------------------------------
|
||||||
@ -420,7 +515,7 @@ func ircFnmatch(pattern string, s string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reMsg = regexp.MustCompile(
|
var reMsg = regexp.MustCompile(
|
||||||
`^(@[^ ]* +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
|
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
|
||||||
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
|
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
|
||||||
|
|
||||||
type message struct {
|
type message struct {
|
||||||
@ -563,7 +658,7 @@ func ircIsValidFingerprint(fp string) bool {
|
|||||||
|
|
||||||
// --- Clients (equals users) --------------------------------------------------
|
// --- Clients (equals users) --------------------------------------------------
|
||||||
|
|
||||||
type connCloseWrite interface {
|
type connCloseWriter interface {
|
||||||
net.Conn
|
net.Conn
|
||||||
CloseWrite() error
|
CloseWrite() error
|
||||||
}
|
}
|
||||||
@ -587,15 +682,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type client struct {
|
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 connCloseWriter // high-level connection
|
||||||
recvQ []byte // unprocessed input
|
recvQ []byte // unprocessed input
|
||||||
sendQ []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
|
||||||
killTimer *time.Timer // hard kill timeout
|
killTimer *time.Timer // hard kill timeout
|
||||||
|
|
||||||
opened time.Time // when the connection was opened
|
opened time.Time // when the connection was opened
|
||||||
nSentMessages uint // number of sent messages total
|
nSentMessages uint // number of sent messages total
|
||||||
@ -793,10 +888,10 @@ var (
|
|||||||
// Forcefully tear down all connections.
|
// Forcefully tear down all connections.
|
||||||
func forceQuit(reason string) {
|
func forceQuit(reason string) {
|
||||||
if !quitting {
|
if !quitting {
|
||||||
log.Fatalln("forceQuit called without initiateQuit")
|
exitFatal("forceQuit called without initiateQuit")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("forced shutdown (%s)\n", reason)
|
printStatus("forced shutdown (%s)", reason)
|
||||||
for c := range clients {
|
for c := range clients {
|
||||||
// initiateQuit has already unregistered the client.
|
// initiateQuit has already unregistered the client.
|
||||||
c.kill("Shutting down")
|
c.kill("Shutting down")
|
||||||
@ -805,10 +900,10 @@ func forceQuit(reason string) {
|
|||||||
|
|
||||||
// Initiate a clean shutdown of the whole daemon.
|
// Initiate a clean shutdown of the whole daemon.
|
||||||
func initiateQuit() {
|
func initiateQuit() {
|
||||||
log.Println("shutting down")
|
printStatus("shutting down")
|
||||||
for _, ln := range listeners {
|
for _, ln := range listeners {
|
||||||
if err := ln.Close(); err != nil {
|
if err := ln.Close(); err != nil {
|
||||||
log.Println(err)
|
printError("%s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for c := range clients {
|
for c := range clients {
|
||||||
@ -857,6 +952,12 @@ func ircNotifyRoommates(c *client, message string) {
|
|||||||
|
|
||||||
// --- Clients (continued) -----------------------------------------------------
|
// --- Clients (continued) -----------------------------------------------------
|
||||||
|
|
||||||
|
func (c *client) printDebug(format string, args ...interface{}) {
|
||||||
|
if debugMode {
|
||||||
|
printDebug("(%s) %s", c.address, fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ircAppendClientModes(m uint, mode []byte) []byte {
|
func ircAppendClientModes(m uint, mode []byte) []byte {
|
||||||
if 0 != m&ircUserModeInvisible {
|
if 0 != m&ircUserModeInvisible {
|
||||||
mode = append(mode, 'i')
|
mode = append(mode, 'i')
|
||||||
@ -903,6 +1004,8 @@ func (c *client) send(line string) {
|
|||||||
bytes = bytes[:ircMaxMessageLength]
|
bytes = bytes[:ircMaxMessageLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.printDebug("<- %s", bytes)
|
||||||
|
|
||||||
// TODO: Kill the connection above some "SendQ" threshold (careful!)
|
// TODO: Kill the connection above some "SendQ" threshold (careful!)
|
||||||
c.sendQ = append(c.sendQ, bytes...)
|
c.sendQ = append(c.sendQ, bytes...)
|
||||||
c.sendQ = append(c.sendQ, "\r\n"...)
|
c.sendQ = append(c.sendQ, "\r\n"...)
|
||||||
@ -954,12 +1057,12 @@ func (c *client) unregister(reason string) {
|
|||||||
// Close the connection and forget about the client.
|
// Close the connection and forget about the client.
|
||||||
func (c *client) kill(reason string) {
|
func (c *client) kill(reason string) {
|
||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "Client exited"
|
c.unregister("Client exited")
|
||||||
|
} else {
|
||||||
|
c.unregister(reason)
|
||||||
}
|
}
|
||||||
c.unregister(reason)
|
|
||||||
|
|
||||||
// TODO: Log the address; seems like we always have c.address.
|
c.printDebug("client destroyed (%s)", reason)
|
||||||
log.Println("client destroyed")
|
|
||||||
|
|
||||||
// Try to send a "close notify" alert if the TLS object is ready,
|
// Try to send a "close notify" alert if the TLS object is ready,
|
||||||
// otherwise just tear down the transport.
|
// otherwise just tear down the transport.
|
||||||
@ -2852,14 +2955,14 @@ func ircProcessMessage(c *client, msg *message, raw string) {
|
|||||||
|
|
||||||
// Handle the results from initializing the client's connection.
|
// Handle the results from initializing the client's connection.
|
||||||
func (c *client) onPrepared(host string, isTLS bool) {
|
func (c *client) onPrepared(host string, isTLS bool) {
|
||||||
|
c.printDebug("client resolved to %s, TLS %t", host, isTLS)
|
||||||
if !isTLS {
|
if !isTLS {
|
||||||
c.conn = c.transport.(connCloseWrite)
|
c.conn = c.transport.(connCloseWriter)
|
||||||
} else if tlsConf != nil {
|
} else if tlsConf != nil {
|
||||||
c.tls = tls.Server(c.transport, tlsConf)
|
c.tls = tls.Server(c.transport, tlsConf)
|
||||||
c.conn = c.tls
|
c.conn = c.tls
|
||||||
} else {
|
} else {
|
||||||
log.Printf("could not initialize TLS for %s: TLS support disabled\n",
|
c.printDebug("could not initialize TLS: disabled")
|
||||||
c.address)
|
|
||||||
c.kill("TLS support disabled")
|
c.kill("TLS support disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2892,10 +2995,10 @@ func (c *client) onRead(data []byte, readErr error) {
|
|||||||
// XXX: And since it accepts LF, we miscalculate receivedBytes within.
|
// XXX: And since it accepts LF, we miscalculate receivedBytes within.
|
||||||
c.recvQ = c.recvQ[advance:]
|
c.recvQ = c.recvQ[advance:]
|
||||||
line := string(token)
|
line := string(token)
|
||||||
log.Printf("-> %s\n", line)
|
c.printDebug("-> %s", line)
|
||||||
|
|
||||||
if msg := ircParseMessage(line); msg == nil {
|
if msg := ircParseMessage(line); msg == nil {
|
||||||
log.Println("error: invalid line")
|
c.printDebug("error: invalid line")
|
||||||
} else {
|
} else {
|
||||||
ircProcessMessage(c, msg, line)
|
ircProcessMessage(c, msg, line)
|
||||||
}
|
}
|
||||||
@ -2905,18 +3008,17 @@ func (c *client) onRead(data []byte, readErr error) {
|
|||||||
c.reading = false
|
c.reading = false
|
||||||
|
|
||||||
if readErr != io.EOF {
|
if readErr != io.EOF {
|
||||||
log.Println(readErr)
|
c.printDebug("%s", readErr)
|
||||||
c.kill(readErr.Error())
|
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")
|
c.printDebug("client finished shutdown")
|
||||||
c.kill("TODO")
|
c.kill("")
|
||||||
} else {
|
} else {
|
||||||
log.Println("client EOF")
|
c.printDebug("client EOF")
|
||||||
c.closeLink("")
|
c.closeLink("")
|
||||||
}
|
}
|
||||||
} else if len(c.recvQ) > 8192 {
|
} else if len(c.recvQ) > 8192 {
|
||||||
log.Println("client recvQ overrun")
|
|
||||||
c.closeLink("recvQ 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
|
||||||
@ -2941,7 +3043,7 @@ func (c *client) onWrite(written int, writeErr error) {
|
|||||||
c.writing = false
|
c.writing = false
|
||||||
|
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
log.Println(writeErr)
|
c.printDebug("%s", writeErr)
|
||||||
c.kill(writeErr.Error())
|
c.kill(writeErr.Error())
|
||||||
} else if len(c.sendQ) > 0 {
|
} else if len(c.sendQ) > 0 {
|
||||||
c.flushSendQ()
|
c.flushSendQ()
|
||||||
@ -2949,7 +3051,7 @@ func (c *client) onWrite(written int, writeErr error) {
|
|||||||
if c.reading {
|
if c.reading {
|
||||||
c.conn.CloseWrite()
|
c.conn.CloseWrite()
|
||||||
} else {
|
} else {
|
||||||
c.kill("TODO")
|
c.kill("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2968,9 +3070,9 @@ func accept(ln net.Listener) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if op, ok := err.(net.Error); !ok || !op.Temporary() {
|
if op, ok := err.(net.Error); !ok || !op.Temporary() {
|
||||||
log.Fatalln(err)
|
exitFatal("%s", err)
|
||||||
} else {
|
} else {
|
||||||
log.Println(err)
|
printError("%s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TCP_NODELAY is set by default on TCPConns.
|
// TCP_NODELAY is set by default on TCPConns.
|
||||||
@ -2980,12 +3082,7 @@ func accept(ln net.Listener) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepare(client *client) {
|
func prepare(client *client) {
|
||||||
conn := client.transport
|
conn, host := client.transport, client.hostname
|
||||||
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
// In effect, we require TCP/UDP, as they have port numbers.
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
|
// The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
|
||||||
// bothering with pointless contexts.
|
// bothering with pointless contexts.
|
||||||
@ -2993,7 +3090,7 @@ func prepare(client *client) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
if names, err := net.LookupAddr(host); err != nil {
|
if names, err := net.LookupAddr(host); err != nil {
|
||||||
log.Println(err)
|
printError("%s", err)
|
||||||
} else {
|
} else {
|
||||||
ch <- names[0]
|
ch <- names[0]
|
||||||
}
|
}
|
||||||
@ -3013,7 +3110,7 @@ func prepare(client *client) {
|
|||||||
isTLS := false
|
isTLS := false
|
||||||
if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
|
if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
|
||||||
// This is just for the TLS detection and doesn't need to be fatal.
|
// This is just for the TLS detection and doesn't need to be fatal.
|
||||||
log.Println(err)
|
printError("%s", err)
|
||||||
} else {
|
} else {
|
||||||
isTLS = detectTLS(sysconn)
|
isTLS = detectTLS(sysconn)
|
||||||
}
|
}
|
||||||
@ -3062,18 +3159,16 @@ func processOneEvent() {
|
|||||||
|
|
||||||
case conn := <-conns:
|
case conn := <-conns:
|
||||||
if maxConnections > 0 && len(clients) >= maxConnections {
|
if maxConnections > 0 && len(clients) >= maxConnections {
|
||||||
log.Println("connection limit reached, refusing connection")
|
printDebug("connection limit reached, refusing connection")
|
||||||
conn.Close()
|
conn.Close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("accepted client connection")
|
|
||||||
|
|
||||||
// In effect, we require TCP/UDP, as they have port numbers.
|
|
||||||
address := conn.RemoteAddr().String()
|
address := conn.RemoteAddr().String()
|
||||||
host, port, err := net.SplitHostPort(address)
|
host, port, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
// In effect, we require TCP/UDP, as they have port numbers.
|
||||||
|
exitFatal("%s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &client{
|
c := &client{
|
||||||
@ -3087,26 +3182,25 @@ func processOneEvent() {
|
|||||||
// TODO: Make this configurable and more fine-grained.
|
// TODO: Make this configurable and more fine-grained.
|
||||||
antiflood: newFloodDetector(10*time.Second, 20),
|
antiflood: newFloodDetector(10*time.Second, 20),
|
||||||
}
|
}
|
||||||
|
|
||||||
clients[c] = true
|
clients[c] = true
|
||||||
|
c.printDebug("new client")
|
||||||
go prepare(c)
|
go prepare(c)
|
||||||
|
|
||||||
// The TLS autodetection in prepare needs to have a timeout.
|
// The TLS autodetection in prepare needs to have a timeout.
|
||||||
c.setKillTimer()
|
c.setKillTimer()
|
||||||
|
|
||||||
case ev := <-prepared:
|
case ev := <-prepared:
|
||||||
log.Println("client is ready:", ev.host)
|
|
||||||
if _, ok := clients[ev.client]; ok {
|
if _, ok := clients[ev.client]; ok {
|
||||||
ev.client.onPrepared(ev.host, ev.isTLS)
|
ev.client.onPrepared(ev.host, ev.isTLS)
|
||||||
}
|
}
|
||||||
|
|
||||||
case ev := <-reads:
|
case ev := <-reads:
|
||||||
log.Println("received data from client")
|
|
||||||
if _, ok := clients[ev.client]; ok {
|
if _, ok := clients[ev.client]; ok {
|
||||||
ev.client.onRead(ev.data, ev.err)
|
ev.client.onRead(ev.data, ev.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case ev := <-writes:
|
case ev := <-writes:
|
||||||
log.Println("sent data to client")
|
|
||||||
if _, ok := clients[ev.client]; ok {
|
if _, ok := clients[ev.client]; ok {
|
||||||
ev.client.onWrite(ev.written, ev.err)
|
ev.client.onWrite(ev.written, ev.err)
|
||||||
}
|
}
|
||||||
@ -3296,6 +3390,7 @@ func ircSetupListenFDs() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
listeners = append(listeners, ln)
|
listeners = append(listeners, ln)
|
||||||
|
printStatus("listening on %s", address)
|
||||||
}
|
}
|
||||||
if len(listeners) == 0 {
|
if len(listeners) == 0 {
|
||||||
return errors.New("network setup failed: no ports to listen on")
|
return errors.New("network setup failed: no ports to listen on")
|
||||||
@ -3309,10 +3404,11 @@ func ircSetupListenFDs() error {
|
|||||||
// --- Main --------------------------------------------------------------------
|
// --- Main --------------------------------------------------------------------
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.BoolVar(&debugMode, "debug", false, "run in debug mode")
|
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
|
||||||
version := flag.Bool("version", false, "show version and exit")
|
version := flag.Bool("version", false, "show version and exit")
|
||||||
writeDefaultCfg := flag.Bool("writedefaultcfg", false,
|
writeDefaultCfg := flag.Bool("writedefaultcfg", false,
|
||||||
"write a default configuration file and exit")
|
"write a default configuration file and exit")
|
||||||
|
systemd := flag.Bool("systemd", false, "log in systemd format")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -3324,6 +3420,9 @@ func main() {
|
|||||||
callSimpleConfigWriteDefault("", configTable)
|
callSimpleConfigWriteDefault("", configTable)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if *systemd {
|
||||||
|
logMessage = logMessageSystemd
|
||||||
|
}
|
||||||
if flag.NArg() > 0 {
|
if flag.NArg() > 0 {
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@ -3332,7 +3431,7 @@ func main() {
|
|||||||
config = make(simpleConfig)
|
config = make(simpleConfig)
|
||||||
config.loadDefaults(configTable)
|
config.loadDefaults(configTable)
|
||||||
if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
|
if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
|
||||||
log.Println("error loading configuration", err)
|
printError("error loading configuration: %s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3348,7 +3447,7 @@ func main() {
|
|||||||
ircSetupListenFDs,
|
ircSetupListenFDs,
|
||||||
} {
|
} {
|
||||||
if err := fn(); err != nil {
|
if err := fn(); err != nil {
|
||||||
log.Fatalln(err)
|
exitFatal("%s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
168
hid/main_test.go
Normal file
168
hid/main_test.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
//
|
||||||
|
// Copyright (c) 2015 - 2018, Přemysl Janouch <p@janouch.name>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
//
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitString(t *testing.T) {
|
||||||
|
var splitStringTests = []struct {
|
||||||
|
s, delims string
|
||||||
|
ignoreEmpty bool
|
||||||
|
result []string
|
||||||
|
}{
|
||||||
|
{",a,,bc", ",", false, []string{"", "a", "", "bc"}},
|
||||||
|
{",a,,bc", ",", true, []string{"a", "bc"}},
|
||||||
|
{"a,;bc,", ",;", false, []string{"a", "", "bc", ""}},
|
||||||
|
{"a,;bc,", ",;", true, []string{"a", "bc"}},
|
||||||
|
{"", ",", false, []string{""}},
|
||||||
|
{"", ",", true, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range splitStringTests {
|
||||||
|
got := splitString(d.s, d.delims, d.ignoreEmpty)
|
||||||
|
if !reflect.DeepEqual(got, d.result) {
|
||||||
|
t.Errorf("case %d: %v should be %v\n", i, got, d.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketpair() (*os.File, *os.File, error) {
|
||||||
|
pair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// See go #24331, this makes 1.11 use the internal poller
|
||||||
|
// while there wasn't a way to achieve that before.
|
||||||
|
if err := syscall.SetNonblock(int(pair[0]), true); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err := syscall.SetNonblock(int(pair[1]), true); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fa := os.NewFile(uintptr(pair[0]), "a")
|
||||||
|
if fa == nil {
|
||||||
|
return nil, nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
fb := os.NewFile(uintptr(pair[1]), "b")
|
||||||
|
if fb == nil {
|
||||||
|
fa.Close()
|
||||||
|
return nil, nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return fa, fb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectTLS(t *testing.T) {
|
||||||
|
detectTLSFromFunc := func(t *testing.T, writer func(net.Conn)) bool {
|
||||||
|
// net.Pipe doesn't use file descriptors, we need a socketpair.
|
||||||
|
sockA, sockB, err := socketpair()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer sockA.Close()
|
||||||
|
defer sockB.Close()
|
||||||
|
|
||||||
|
fcB, err := net.FileConn(sockB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
go writer(fcB)
|
||||||
|
|
||||||
|
fcA, err := net.FileConn(sockA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sc, err := fcA.(syscall.Conn).SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return detectTLS(sc)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("SSL_2.0", func(t *testing.T) {
|
||||||
|
if !detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
// The obsolete, useless, unsupported SSL 2.0 record format.
|
||||||
|
_, _ = fc.Write([]byte{0x80, 0x01, 0x01})
|
||||||
|
}) {
|
||||||
|
t.Error("could not detect SSL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("crypto_tls", func(t *testing.T) {
|
||||||
|
if !detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
conn := tls.Client(fc, &tls.Config{InsecureSkipVerify: true})
|
||||||
|
_ = conn.Handshake()
|
||||||
|
}) {
|
||||||
|
t.Error("could not detect TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("text", func(t *testing.T) {
|
||||||
|
if detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
_, _ = fc.Write([]byte("ПРЕВЕД"))
|
||||||
|
}) {
|
||||||
|
t.Error("detected UTF-8 as TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EOF", func(t *testing.T) {
|
||||||
|
type connCloseWriter interface {
|
||||||
|
net.Conn
|
||||||
|
CloseWrite() error
|
||||||
|
}
|
||||||
|
if detectTLSFromFunc(t, func(fc net.Conn) {
|
||||||
|
_ = fc.(connCloseWriter).CloseWrite()
|
||||||
|
}) {
|
||||||
|
t.Error("detected EOF as TLS")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIRC(t *testing.T) {
|
||||||
|
msg := ircParseMessage(
|
||||||
|
`@first=a\:\s\r\n\\;2nd :srv hi there :good m8 :how are you?`)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(msg.tags, map[string]string{
|
||||||
|
"first": "a; \r\n\\",
|
||||||
|
"2nd": "",
|
||||||
|
}) {
|
||||||
|
t.Error("tags parsed incorrectly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.nick != "srv" || msg.user != "" || msg.host != "" {
|
||||||
|
t.Error("server name parsed incorrectly")
|
||||||
|
}
|
||||||
|
if msg.command != "hi" {
|
||||||
|
t.Error("command name parsed incorrectly")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(msg.params,
|
||||||
|
[]string{"there", "good m8 :how are you?"}) {
|
||||||
|
t.Error("params parsed incorrectly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ircEqual("[fag]^", "{FAG}~") {
|
||||||
|
t.Error("string case comparison not according to RFC 2812")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: More tests.
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user