Compare commits

..

No commits in common. "f8bcfe447c748b47681afaa996a782768fabf30b" and "23f637dd47837a6f52e73bca42e14b1923a6dc95" have entirely different histories.

3 changed files with 72 additions and 347 deletions

14
README
View File

@ -108,7 +108,7 @@ The c2 wiki unsurprisingly has a lot of material around the design and
realisation of GUIs, which might be useful.
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 use fixed
first harder problems here. However I certainly don't want to used fixed
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
@ -117,18 +117,13 @@ 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
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
~~~~~~~~~~~~~~~~~~~~
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.
Go also has x/text packages for this purpose, which might be better than GNU,
but they're essentially still in development.
but is essentially still in development.
Versioning
~~~~~~~~~~
@ -196,7 +191,7 @@ takes with my 10 dictionaries isn't particularly bad), pack everything with
archive/zip.
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 index to be ordered by the
Since we have our own format, we may expect the indexed to be ordered by the
locale's rules, assuming they don't change between versions.
hmpc -- MPD client
@ -225,9 +220,6 @@ 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,
which may be have names starting on "// ".
Resources:
- http://doc.cat-v.org/plan_9/4th_edition/papers/sam/
hfm -- file manager
~~~~~~~~~~~~~~~~~~~
All we need to achieve here is replace Midnight Commander, which besides the

View File

@ -27,7 +27,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log/syslog"
"log"
"net"
"os"
"os/signal"
@ -48,100 +48,6 @@ const (
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 ---------------------------------------------------------------
// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
@ -258,7 +164,9 @@ func findTildeHome(username string) string {
} else if v, ok := os.LookupEnv("HOME"); ok {
return v
}
printDebug("failed to expand the home directory for %s", username)
if debugMode {
log.Printf("failed to expand the home directory for %s", username)
}
return "~" + username
}
@ -284,30 +192,27 @@ func resolveFilename(filename string, relativeCB func(string) string) string {
// --- Simple file I/O ---------------------------------------------------------
// Overwrites filename contents with data; creates directories as needed.
func writeFile(path string, data []byte) error {
if dir := filepath.Dir(path); dir != "." {
func writeFile(filename string, data []byte) error {
if dir := filepath.Dir(filename); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
}
return ioutil.WriteFile(path, data, 0644)
return ioutil.WriteFile(filename, data, 0644)
}
// Wrapper for writeFile that makes sure that the new data has been written
// to disk in its entirety before overriding the old file.
func writeFileSafe(path string, data []byte) error {
temp := path + ".new"
func writeFileSafe(filename string, data []byte) error {
temp := filename + ".new"
if err := writeFile(temp, data); err != nil {
return err
}
return os.Rename(temp, path)
return os.Rename(temp, filename)
}
// --- Simple configuration ----------------------------------------------------
// This is the bare minimum to make an application configurable.
// Keys are stripped of surrounding whitespace, values are not.
type simpleConfigItem struct {
key string // INI key
def string // default value
@ -324,8 +229,8 @@ func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
func (sc simpleConfig) updateFromFile() error {
basename := projectName + ".conf"
path := resolveFilename(basename, resolveRelativeConfigFilename)
if path == "" {
filename := resolveFilename(basename, resolveRelativeConfigFilename)
if filename == "" {
return &os.PathError{
Op: "cannot find",
Path: basename,
@ -333,7 +238,7 @@ func (sc simpleConfig) updateFromFile() error {
}
}
f, err := os.Open(path)
f, err := os.Open(filename)
if err != nil {
return err
}
@ -348,7 +253,7 @@ func (sc simpleConfig) updateFromFile() error {
equals := strings.IndexByte(line, '=')
if equals <= 0 {
return fmt.Errorf("%s:%d: malformed line", path, lineNo)
return fmt.Errorf("%s:%d: malformed line", filename, lineNo)
}
sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
@ -390,13 +295,13 @@ func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
``,
}
path, err := simpleConfigWriteDefault(
filename, err := simpleConfigWriteDefault(
pathHint, strings.Join(prologLines, "\n"), table)
if err != nil {
exitFatal("%s", err)
log.Fatalln(err)
}
printStatus("configuration written to `%s'", path)
log.Printf("configuration written to `%s'\n", filename)
}
// --- Configuration -----------------------------------------------------------
@ -515,7 +420,7 @@ func ircFnmatch(pattern string, s string) bool {
}
var reMsg = regexp.MustCompile(
`^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
`^(@[^ ]* +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
type message struct {
@ -658,7 +563,7 @@ func ircIsValidFingerprint(fp string) bool {
// --- Clients (equals users) --------------------------------------------------
type connCloseWriter interface {
type connCloseWrite interface {
net.Conn
CloseWrite() error
}
@ -684,7 +589,7 @@ const (
type client struct {
transport net.Conn // underlying connection
tls *tls.Conn // TLS, if detected
conn connCloseWriter // high-level connection
conn connCloseWrite // high-level connection
recvQ []byte // unprocessed input
sendQ []byte // unprocessed output
reading bool // whether a reading goroutine is running
@ -888,10 +793,10 @@ var (
// Forcefully tear down all connections.
func forceQuit(reason string) {
if !quitting {
exitFatal("forceQuit called without initiateQuit")
log.Fatalln("forceQuit called without initiateQuit")
}
printStatus("forced shutdown (%s)", reason)
log.Printf("forced shutdown (%s)\n", reason)
for c := range clients {
// initiateQuit has already unregistered the client.
c.kill("Shutting down")
@ -900,10 +805,10 @@ func forceQuit(reason string) {
// Initiate a clean shutdown of the whole daemon.
func initiateQuit() {
printStatus("shutting down")
log.Println("shutting down")
for _, ln := range listeners {
if err := ln.Close(); err != nil {
printError("%s", err)
log.Println(err)
}
}
for c := range clients {
@ -952,12 +857,6 @@ func ircNotifyRoommates(c *client, message string) {
// --- 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 {
if 0 != m&ircUserModeInvisible {
mode = append(mode, 'i')
@ -1004,8 +903,6 @@ func (c *client) send(line string) {
bytes = bytes[:ircMaxMessageLength]
}
c.printDebug("<- %s", bytes)
// TODO: Kill the connection above some "SendQ" threshold (careful!)
c.sendQ = append(c.sendQ, bytes...)
c.sendQ = append(c.sendQ, "\r\n"...)
@ -1057,12 +954,12 @@ func (c *client) unregister(reason string) {
// Close the connection and forget about the client.
func (c *client) kill(reason string) {
if reason == "" {
c.unregister("Client exited")
} else {
c.unregister(reason)
reason = "Client exited"
}
c.unregister(reason)
c.printDebug("client destroyed (%s)", reason)
// TODO: Log the address; seems like we always have c.address.
log.Println("client destroyed")
// Try to send a "close notify" alert if the TLS object is ready,
// otherwise just tear down the transport.
@ -2955,14 +2852,14 @@ func ircProcessMessage(c *client, msg *message, raw string) {
// Handle the results from initializing the client's connection.
func (c *client) onPrepared(host string, isTLS bool) {
c.printDebug("client resolved to %s, TLS %t", host, isTLS)
if !isTLS {
c.conn = c.transport.(connCloseWriter)
c.conn = c.transport.(connCloseWrite)
} else if tlsConf != nil {
c.tls = tls.Server(c.transport, tlsConf)
c.conn = c.tls
} else {
c.printDebug("could not initialize TLS: disabled")
log.Printf("could not initialize TLS for %s: TLS support disabled\n",
c.address)
c.kill("TLS support disabled")
return
}
@ -2995,10 +2892,10 @@ func (c *client) onRead(data []byte, readErr error) {
// XXX: And since it accepts LF, we miscalculate receivedBytes within.
c.recvQ = c.recvQ[advance:]
line := string(token)
c.printDebug("-> %s", line)
log.Printf("-> %s\n", line)
if msg := ircParseMessage(line); msg == nil {
c.printDebug("error: invalid line")
log.Println("error: invalid line")
} else {
ircProcessMessage(c, msg, line)
}
@ -3008,17 +2905,18 @@ func (c *client) onRead(data []byte, readErr error) {
c.reading = false
if readErr != io.EOF {
c.printDebug("%s", readErr)
log.Println(readErr)
c.kill(readErr.Error())
} else if c.closing {
// Disregarding whether a clean shutdown has happened or not.
c.printDebug("client finished shutdown")
c.kill("")
log.Println("client finished shutdown")
c.kill("TODO")
} else {
c.printDebug("client EOF")
log.Println("client EOF")
c.closeLink("")
}
} else if len(c.recvQ) > 8192 {
log.Println("client recvQ overrun")
c.closeLink("recvQ overrun")
// tls.Conn doesn't have the CloseRead method (and it needs to be able
@ -3043,7 +2941,7 @@ func (c *client) onWrite(written int, writeErr error) {
c.writing = false
if writeErr != nil {
c.printDebug("%s", writeErr)
log.Println(writeErr)
c.kill(writeErr.Error())
} else if len(c.sendQ) > 0 {
c.flushSendQ()
@ -3051,7 +2949,7 @@ func (c *client) onWrite(written int, writeErr error) {
if c.reading {
c.conn.CloseWrite()
} else {
c.kill("")
c.kill("TODO")
}
}
}
@ -3070,9 +2968,9 @@ func accept(ln net.Listener) {
return
}
if op, ok := err.(net.Error); !ok || !op.Temporary() {
exitFatal("%s", err)
log.Fatalln(err)
} else {
printError("%s", err)
log.Println(err)
}
} else {
// TCP_NODELAY is set by default on TCPConns.
@ -3082,7 +2980,12 @@ func accept(ln net.Listener) {
}
func prepare(client *client) {
conn, host := client.transport, client.hostname
conn := client.transport
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
// bothering with pointless contexts.
@ -3090,7 +2993,7 @@ func prepare(client *client) {
go func() {
defer close(ch)
if names, err := net.LookupAddr(host); err != nil {
printError("%s", err)
log.Println(err)
} else {
ch <- names[0]
}
@ -3110,7 +3013,7 @@ func prepare(client *client) {
isTLS := false
if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
// This is just for the TLS detection and doesn't need to be fatal.
printError("%s", err)
log.Println(err)
} else {
isTLS = detectTLS(sysconn)
}
@ -3159,16 +3062,18 @@ func processOneEvent() {
case conn := <-conns:
if maxConnections > 0 && len(clients) >= maxConnections {
printDebug("connection limit reached, refusing connection")
log.Println("connection limit reached, refusing connection")
conn.Close()
break
}
log.Println("accepted client connection")
// In effect, we require TCP/UDP, as they have port numbers.
address := conn.RemoteAddr().String()
host, port, err := net.SplitHostPort(address)
if err != nil {
// In effect, we require TCP/UDP, as they have port numbers.
exitFatal("%s", err)
log.Fatalln(err)
}
c := &client{
@ -3182,25 +3087,26 @@ func processOneEvent() {
// TODO: Make this configurable and more fine-grained.
antiflood: newFloodDetector(10*time.Second, 20),
}
clients[c] = true
c.printDebug("new client")
go prepare(c)
// The TLS autodetection in prepare needs to have a timeout.
c.setKillTimer()
case ev := <-prepared:
log.Println("client is ready:", ev.host)
if _, ok := clients[ev.client]; ok {
ev.client.onPrepared(ev.host, ev.isTLS)
}
case ev := <-reads:
log.Println("received data from client")
if _, ok := clients[ev.client]; ok {
ev.client.onRead(ev.data, ev.err)
}
case ev := <-writes:
log.Println("sent data to client")
if _, ok := clients[ev.client]; ok {
ev.client.onWrite(ev.written, ev.err)
}
@ -3390,7 +3296,6 @@ func ircSetupListenFDs() error {
return err
}
listeners = append(listeners, ln)
printStatus("listening on %s", address)
}
if len(listeners) == 0 {
return errors.New("network setup failed: no ports to listen on")
@ -3404,11 +3309,10 @@ func ircSetupListenFDs() error {
// --- Main --------------------------------------------------------------------
func main() {
flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
flag.BoolVar(&debugMode, "debug", false, "run in debug mode")
version := flag.Bool("version", false, "show version and exit")
writeDefaultCfg := flag.Bool("writedefaultcfg", false,
"write a default configuration file and exit")
systemd := flag.Bool("systemd", false, "log in systemd format")
flag.Parse()
@ -3420,9 +3324,6 @@ func main() {
callSimpleConfigWriteDefault("", configTable)
return
}
if *systemd {
logMessage = logMessageSystemd
}
if flag.NArg() > 0 {
flag.Usage()
os.Exit(2)
@ -3431,7 +3332,7 @@ func main() {
config = make(simpleConfig)
config.loadDefaults(configTable)
if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
printError("error loading configuration: %s", err)
log.Println("error loading configuration", err)
os.Exit(1)
}
@ -3447,7 +3348,7 @@ func main() {
ircSetupListenFDs,
} {
if err := fn(); err != nil {
exitFatal("%s", err)
log.Fatalln(err)
}
}

View File

@ -1,168 +0,0 @@
//
// 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.
}