aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--LICENSE12
-rw-r--r--README264
-rwxr-xr-xhid/hid-gen-replies.sh16
-rw-r--r--hid/hid-replies87
-rw-r--r--hid/main.go3457
-rw-r--r--hid/main_test.go168
-rw-r--r--hnc/main.go150
-rw-r--r--prototypes/tls-autodetect.go451
-rw-r--r--prototypes/xgb-draw.go329
-rw-r--r--prototypes/xgb-image.go313
-rw-r--r--prototypes/xgb-keys.go213
-rw-r--r--prototypes/xgb-monitors.go40
-rw-r--r--prototypes/xgb-text-viewer.go538
-rw-r--r--prototypes/xgb-window.go156
-rw-r--r--prototypes/xgb-xrender.go397
16 files changed, 6592 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c6b6b3e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 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.
diff --git a/README b/README
new file mode 100644
index 0000000..2095516
--- /dev/null
+++ b/README
@@ -0,0 +1,264 @@
+Project haven
+=============
+
+haven is an umbrella project for a range of mostly desktop applications.
+
+Goal
+----
+The greater goal is to create a fresh computing environment for daily work and
+play--easily controllable, reasonably complex, both visually and internally
+unified and last but not least responsive. One should be able to use it
+comfortably with a 60% keyboard and no pointing device.
+
+haven serves as a testing ground, leaning on the side of compromises. It aims
+to use today's Linux desktop as a support, relying on X11/Wayland, existing
+window managers and web browsers.
+
+The focus is therefore on going breadth-first, not depth-first. Applications
+only need to be good enough to be able to replace their older siblings at all.
+I.e. for me personally.
+
+Scope
+-----
+Subproject names aim to have the minimum viable, reasonably identifiable name.
+To group them together, a common prefix of "h" is used. The second column is
+what should be used as the name in .desktop files, just like the GNOME project
+figured out it would make sense:
+
+ - hbe - bitmap editor
+ - hbfe - bitmap font editor
+ - he - text editor
+ - hfm - file manager
+ - hib - IRC bouncer
+ - hic - IRC client
+ - hid - IRC daemon
+ - hiv - image viewer
+ - hm - mail client
+ - hmpc - MPD client
+ - hnc - netcat-alike
+ - ho - all-powerful organizer
+ - hsm - system monitor
+ - hss - spreadsheets
+ - htd - translation dictionary
+ - ht - terminal emulator
+ - htk - GUI toolkit library
+
+See Projects for more information about the individual projects.
+
+Some taken names in Debian: hd (important), hte, hy.
+
+Identity
+--------
+The name merely hints at motivations and otherwise isn't of any practical
+significance other than that we need an identifier. This time I resisted using
+something offensive for personal amusement.
+
+A logo covering the entire project is not needed and it seems hard to figure out
+anything meaninful anyway, though we might pick a specific font to use for the
+project name <1>.
+
+The only mascot I can think of would be a black and white or generally grayscale
+My Little Pony OC but I don't really want to bring my own kinks into the project
+and I'd also need to learn how to draw one so that I don't infringe on someone
+else's copyright, or find someone else to do it. Anyway, in lack of a proper
+logo, she could have a simple "h" or "hvn" for a cutie mark <2>.
+
+ __
+ <2> _/ /_ <1> | _ _ _ _
+ \ _ \ |/ \ / \| \ / /_\ |/ \
+ / / / / | | | | \ / | | |
+ /_/ /_/ | | \_/| \/ \_/ | |
+
+I'm not sure where I took this "h" letter styling from, it seems too familiar.
+The above illustrations also show how awful it looks when a logo is just
+a stylized version of the first letter of a name when you put the two next to
+each other. Distinctly redundant. Facebook and Twitter are doing fine, though,
+perhaps because they do not use them together like that.
+
+Technicalities
+--------------
+
+Languages
+~~~~~~~~~
+Primarily Golang with limited C interfacing glue, secondarily C++17, mostly for
+when the former cannot be reasonably used because of dependencies.
+
+Build system
+~~~~~~~~~~~~
+https://github.com/zevv/bucklespring is a good example of a rather simplified
+project that makes do with a single Makefile, even for cross-compilation on
+Windows. Let us avoid CMake and the likes of it.
+
+It seems that Go can link dynamically, therefore I could build libhaven.so
+https://docs.google.com/document/d/1nr-TQHw_er6GOQRsF6T43GGhFDelrAP0NqSS_00RgZQ
+https://stackoverflow.com/questions/1757090/shared-library-in-go
+and have the rest of the package as rather small binaries linking to it.
+The "cannot implicitly include runtime/cgo in a shared library" error is solved
+by "go install", which again requires "-pkgdir" because of privileges.
+libstd.so is a beautiful 30 megabytes (compared to libc.a: 4.9M).
+
+GUI
+~~~
+Probably build on top of X11/Xlib or xgb<1>. Wayland can wait until it
+stabilizes--it should not be a major issue switching the backends.
+Vector graphics can be handled by draw2d<2>.
+<1> https://rosettacode.org/wiki/Window_creation/X11#Go
+<2> https://github.com/llgcode/draw2d
+
+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
+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
+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.
+
+Versioning
+~~~~~~~~~~
+Versions are for end users. We can use a zero-based, strictly increasing
+number, be it a simple sequence or date-based numbers such as 1807. If we do
+this at all, we will eventually also need to release patch versions.
+
+Since dates don't seem to convey important information, let us settle on 0, 1,
+1.1, 2, 3, 3.1, 3.2, ... In practice releases are going to be scarce, unless
+we find a person to take care of it. Note that there is no major/minor pair--
+a new project will be created when radical philosophical or architectural
+changes are to be made. See Goal.
+
+Projects
+--------
+These are sorted in the order in which they should be created in order to gain
+the best possible momentum. The htk GUI toolkit is implied as a side product
+permeating the entire list.
+
+Some information is omitted from these descriptions and lies either in my head
+or in my other notes.
+
+hid -- IRC daemon
+~~~~~~~~~~~~~~~~~
+This project is unimportant by itself, its sole purpose is to gain experience
+with Go on something that I have already done and understand well. Nothing
+beyond achieving feature parity is in the initial scope.
+
+One possibility of complicating would be adding simple WebSocket listeners but
+that's already been done for me https://github.com/kiwiirc/webircgateway and
+it's even in Go, I just need to set up kiwiirc.
+
+Later, when we have a pleasant IRC client, implement either the P10 or the TS6
+server-linking protocol and make atheme work with a generic module.
+Alternatively add support for plugins. The goal is to allow creating integrated
+bridges to various public forums.
+
+hnc -- netcat-alike
+~~~~~~~~~~~~~~~~~~~
+The result of testing hid with telnet, OpenSSL s_client, OpenBSD nc, GNU nc and
+Ncat is that neither of them can properly shutdown the connection. We need
+a good implementation with TLS support.
+
+hib and hic -- IRC bouncer and client
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+An IRC client is a good starting application for building a GUI toolkit, as the
+UI can afford to be truly minimalistic and most of it is text.
+
+To resolve an issue I have with my current IRC client, the client is going to be
+split into two parts: a bouncer that manages all connections and state, and
+a separate GUI that communicates with the backend over TLS/WebSocket. Perhaps
+only the per-buffer input line is going to be desynchronized.
+
+https://godoc.org/github.com/gorilla/websocket
+
+The higher-level client-server API could be made rather generic to allow for
+smooth integration with non-IRC "backends" such as Slack or Mattermost.
+
+htd -- translation dictionary
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This specific kind of application doesn't need a lot of user interface either,
+just a tab bar, text entry and two columns of text with simple formatting.
+
+For simplicity we will establish a custom dictionary format based on either
+simple compress/gzip with separate files in StarDict style or, since we don't
+really strive for random access and memory-efficiency (those 120M that sdtui
+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
+locale's rules, assuming they don't change between versions.
+
+hmpc -- MPD client
+~~~~~~~~~~~~~~~~~~
+Here the focus will be on the GUI toolkit. I don't expect this application to
+get big, since its predecessor nncmpp isn't either. The daemon takes care of
+all complex stuff. It would be nice to add lyrics and search later, though.
+
+hiv -- image viewer
+~~~~~~~~~~~~~~~~~~~
+JPG, PNG, first frame of GIF. Zoom. Going through adjacent files in directory
+using cursor keys. Possibly a dialog with image metadata.
+
+he -- text editor
+~~~~~~~~~~~~~~~~~
+VIM controls, no scripting, no syntax highlight, single-file, made for variable-
+-width/proportional fonts. Initially done primarily to produce a text editing
+widget, which is going to be an interesting challenge, arguably better solved by
+whole program composition. Scintilla may provide some inspiration.
+
+In the second stage, support for the Language Server Protocol will be added so
+that the project can be edited using its own tools. Some scripting, perhaps
+a tiny subset of VimL, might be desirable. Or other means of configuration.
+
+Visual block mode or the color column may still be implemented.
+
+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
+most basic features includes a VFS for archives. The editing widget in read-
+-only mode could be used for F3. The shell is going to work very simply,
+creating a PTY device and running things under TERM=dumb while decoding SGR,
+or one could decide to run a new terminal emulator with a different shortcut.
+
+Eventually the number of panels should be arbitrary with proper shortcuts for
+working with them. We might also integrate a special view for picture previews,
+which might or might not deserve its own program.
+
+ho -- all-powerful organizer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Zettelkasten with fulltext search, arbitrary reciprocal links, arbitrary tags.
+Flat storage. Should be able to use translation dictionaries for search hints.
+
+Indexing and search may be based on a common database, no need to get all fancy:
+http://rachbelaid.com/postgres-full-text-search-is-good-enough/
+https://www.sqlite.org/fts3.html#full_text_index_queries (FTS4 seems better)
+
+ht -- terminal emulator
+~~~~~~~~~~~~~~~~~~~~~~~
+Similar scope to st(1). Clever display of internal padding for better looks.
+
+The rest
+~~~~~~~~
+Currently there are no significant, specific plans about the other applications.
diff --git a/hid/hid-gen-replies.sh b/hid/hid-gen-replies.sh
new file mode 100755
index 0000000..c32b000
--- /dev/null
+++ b/hid/hid-gen-replies.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+LC_ALL=C exec awk '
+ /^[0-9]+ *(ERR|RPL)_[A-Z]+ *".*"$/ {
+ match($0, /".*"/);
+ ids[$1] = $2;
+ texts[$2] = substr($0, RSTART, RLENGTH);
+ }
+ END {
+ print "package " ENVIRON["GOPACKAGE"] "\n\nconst ("
+ for (i in ids)
+ printf("\t%s = %s\n", ids[i], i)
+ print ")\n\nvar defaultReplies = map[int]string{"
+ for (i in ids)
+ print "\t" ids[i] ": " texts[ids[i]] ","
+ print "}"
+ }'
diff --git a/hid/hid-replies b/hid/hid-replies
new file mode 100644
index 0000000..c539520
--- /dev/null
+++ b/hid/hid-replies
@@ -0,0 +1,87 @@
+1 RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s"
+2 RPL_YOURHOST ":Your host is %s, running version %s"
+3 RPL_CREATED ":This server was created %s"
+4 RPL_MYINFO "%s %s %s %s"
+5 RPL_ISUPPORT "%s :are supported by this server"
+211 RPL_STATSLINKINFO "%s %d %d %d %d %d %d"
+212 RPL_STATSCOMMANDS "%s %d %d %d"
+219 RPL_ENDOFSTATS "%c :End of STATS report"
+221 RPL_UMODEIS "+%s"
+242 RPL_STATSUPTIME ":Server Up %d days %d:%02d:%02d"
+251 RPL_LUSERCLIENT ":There are %d users and %d services on %d servers"
+252 RPL_LUSEROP "%d :operator(s) online"
+253 RPL_LUSERUNKNOWN "%d :unknown connection(s)"
+254 RPL_LUSERCHANNELS "%d :channels formed"
+255 RPL_LUSERME ":I have %d clients and %d servers"
+301 RPL_AWAY "%s :%s"
+302 RPL_USERHOST ":%s"
+303 RPL_ISON ":%s"
+305 RPL_UNAWAY ":You are no longer marked as being away"
+306 RPL_NOWAWAY ":You have been marked as being away"
+311 RPL_WHOISUSER "%s %s %s * :%s"
+312 RPL_WHOISSERVER "%s %s :%s"
+313 RPL_WHOISOPERATOR "%s :is an IRC operator"
+314 RPL_WHOWASUSER "%s %s %s * :%s"
+315 RPL_ENDOFWHO "%s :End of WHO list"
+317 RPL_WHOISIDLE "%s %d :seconds idle"
+318 RPL_ENDOFWHOIS "%s :End of WHOIS list"
+319 RPL_WHOISCHANNELS "%s :%s"
+322 RPL_LIST "%s %d :%s"
+323 RPL_LISTEND ":End of LIST"
+324 RPL_CHANNELMODEIS "%s +%s"
+329 RPL_CREATIONTIME "%s %d"
+331 RPL_NOTOPIC "%s :No topic is set"
+332 RPL_TOPIC "%s :%s"
+333 RPL_TOPICWHOTIME "%s %s %d"
+341 RPL_INVITING "%s %s"
+346 RPL_INVITELIST "%s %s"
+347 RPL_ENDOFINVITELIST "%s :End of channel invite list"
+348 RPL_EXCEPTLIST "%s %s"
+349 RPL_ENDOFEXCEPTLIST "%s :End of channel exception list"
+351 RPL_VERSION "%s.%d %s :%s"
+352 RPL_WHOREPLY "%s %s %s %s %s %s :%d %s"
+353 RPL_NAMREPLY "%c %s :%s"
+364 RPL_LINKS "%s %s :%d %s"
+365 RPL_ENDOFLINKS "%s :End of LINKS list"
+366 RPL_ENDOFNAMES "%s :End of NAMES list"
+367 RPL_BANLIST "%s %s"
+368 RPL_ENDOFBANLIST "%s :End of channel ban list"
+369 RPL_ENDOFWHOWAS "%s :End of WHOWAS"
+372 RPL_MOTD ":- %s"
+375 RPL_MOTDSTART ":- %s Message of the day - "
+376 RPL_ENDOFMOTD ":End of MOTD command"
+391 RPL_TIME "%s :%s"
+401 ERR_NOSUCHNICK "%s :No such nick/channel"
+402 ERR_NOSUCHSERVER "%s :No such server"
+403 ERR_NOSUCHCHANNEL "%s :No such channel"
+404 ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel"
+406 ERR_WASNOSUCHNICK "%s :There was no such nickname"
+409 ERR_NOORIGIN ":No origin specified"
+410 ERR_INVALIDCAPCMD "%s :%s"
+411 ERR_NORECIPIENT ":No recipient given (%s)"
+412 ERR_NOTEXTTOSEND ":No text to send"
+421 ERR_UNKNOWNCOMMAND "%s: Unknown command"
+422 ERR_NOMOTD ":MOTD File is missing"
+423 ERR_NOADMININFO "%s :No administrative info available"
+431 ERR_NONICKNAMEGIVEN ":No nickname given"
+432 ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname"
+433 ERR_NICKNAMEINUSE "%s :Nickname is already in use"
+441 ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel"
+442 ERR_NOTONCHANNEL "%s :You're not on that channel"
+443 ERR_USERONCHANNEL "%s %s :is already on channel"
+445 ERR_SUMMONDISABLED ":SUMMON has been disabled"
+446 ERR_USERSDISABLED ":USERS has been disabled"
+451 ERR_NOTREGISTERED ":You have not registered"
+461 ERR_NEEDMOREPARAMS "%s :Not enough parameters"
+462 ERR_ALREADYREGISTERED ":Unauthorized command (already registered)"
+467 ERR_KEYSET "%s :Channel key already set"
+471 ERR_CHANNELISFULL "%s :Cannot join channel (+l)"
+472 ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s"
+473 ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)"
+474 ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)"
+475 ERR_BADCHANNELKEY "%s :Cannot join channel (+k)"
+476 ERR_BADCHANMASK "%s :Bad Channel Mask"
+481 ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator"
+482 ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
+501 ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
+502 ERR_USERSDONTMATCH ":Cannot change mode for other users"
diff --git a/hid/main.go b/hid/main.go
new file mode 100644
index 0000000..cf54ab0
--- /dev/null
+++ b/hid/main.go
@@ -0,0 +1,3457 @@
+//
+// Copyright (c) 2014 - 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.
+//
+
+// hid is a straight-forward port of kike IRCd from C.
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log/syslog"
+ "net"
+ "os"
+ "os/signal"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+)
+
+var debugMode = false
+
+const (
+ projectName = "hid"
+ // TODO: Consider using the same version number for all subprojects.
+ 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.
+func splitString(s, delims string, ignoreEmpty bool) (result []string) {
+ for {
+ end := strings.IndexAny(s, delims)
+ if end < 0 {
+ break
+ }
+ if !ignoreEmpty || end != 0 {
+ result = append(result, s[:end])
+ }
+ s = s[end+1:]
+ }
+ if !ignoreEmpty || s != "" {
+ result = append(result, s)
+ }
+ return
+}
+
+//
+// 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
+// not pose a problem in practice. We might try waiting for them.
+//
+// SSL2: 1xxx xxxx | xxxx xxxx | <1>
+// (message length) (client hello)
+// SSL3/TLS: <22> | <3> | xxxx xxxx
+// (handshake)| (protocol version)
+//
+func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
+ sysconn.Read(func(fd uintptr) (done bool) {
+ var buf [3]byte
+ n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
+ switch {
+ case n == 3:
+ isTLS = buf[0]&0x80 != 0 && buf[2] == 1
+ fallthrough
+ case n == 2:
+ isTLS = isTLS || buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
+ }
+ return true
+ })
+ return isTLS
+}
+
+// --- File system -------------------------------------------------------------
+
+// Look up the value of an XDG path from environment, or fall back to a default.
+func getXDGHomeDir(name, def string) string {
+ env := os.Getenv(name)
+ if env != "" && env[0] == filepath.Separator {
+ return env
+ }
+
+ 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)
+}
+
+func resolveRelativeFilenameGeneric(paths []string, filename string) string {
+ for _, path := range paths {
+ // As per XDG spec, relative paths are ignored.
+ if path == "" || path[0] != filepath.Separator {
+ continue
+ }
+
+ file := filepath.Join(path, filename)
+ if _, err := os.Stat(file); err == nil {
+ return file
+ }
+ }
+ return ""
+}
+
+// Retrieve all XDG base directories for configuration files.
+func getXDGConfigDirs() (result []string) {
+ home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
+ if home != "" {
+ result = append(result, home)
+ }
+ dirs := os.Getenv("XDG_CONFIG_DIRS")
+ if dirs == "" {
+ dirs = "/etc/xdg"
+ }
+ for _, path := range strings.Split(dirs, ":") {
+ if path != "" {
+ result = append(result, path)
+ }
+ }
+ return
+}
+
+func resolveRelativeConfigFilename(filename string) string {
+ return resolveRelativeFilenameGeneric(getXDGConfigDirs(),
+ filepath.Join(projectName, filename))
+}
+
+func findTildeHome(username string) string {
+ if username != "" {
+ if u, _ := user.Lookup(username); u != nil {
+ return u.HomeDir
+ }
+ } else if u, _ := user.Current(); u != nil {
+ return u.HomeDir
+ } else if v, ok := os.LookupEnv("HOME"); ok {
+ return v
+ }
+ printDebug("failed to expand the home directory for %s", username)
+ return "~" + username
+}
+
+func resolveFilename(filename string, relativeCB func(string) string) string {
+ // Absolute path is absolute.
+ if filename == "" || filename[0] == filepath.Separator {
+ return filename
+ }
+ if filename[0] != '~' {
+ return relativeCB(filename)
+ }
+
+ // Paths to home directories ought to be absolute.
+ var n int
+ for n = 0; n < len(filename); n++ {
+ if filename[n] == filepath.Separator {
+ break
+ }
+ }
+ return findTildeHome(filename[1:n]) + filename[n:]
+}
+
+// --- 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 != "." {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+ return ioutil.WriteFile(path, 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"
+ if err := writeFile(temp, data); err != nil {
+ return err
+ }
+ return os.Rename(temp, path)
+}
+
+// --- 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
+ description string // documentation
+}
+
+type simpleConfig map[string]string
+
+func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
+ for _, item := range table {
+ sc[item.key] = item.def
+ }
+}
+
+func (sc simpleConfig) updateFromFile() error {
+ basename := projectName + ".conf"
+ path := resolveFilename(basename, resolveRelativeConfigFilename)
+ if path == "" {
+ return &os.PathError{
+ Op: "cannot find",
+ Path: basename,
+ Err: os.ErrNotExist,
+ }
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for lineNo := 1; scanner.Scan(); lineNo++ {
+ line := strings.TrimLeft(scanner.Text(), " \t")
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ equals := strings.IndexByte(line, '=')
+ if equals <= 0 {
+ return fmt.Errorf("%s:%d: malformed line", path, lineNo)
+ }
+
+ sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
+ }
+ return scanner.Err()
+}
+
+func writeConfigurationFile(pathHint string, data []byte) (string, error) {
+ path := pathHint
+ if path == "" {
+ path = filepath.Join(getXDGHomeDir("XDG_CONFIG_HOME", ".config"),
+ projectName, projectName+".conf")
+ }
+
+ if err := writeFileSafe(path, data); err != nil {
+ return "", err
+ }
+ return path, nil
+}
+
+func simpleConfigWriteDefault(pathHint string, prolog string,
+ table []simpleConfigItem) (string, error) {
+ data := []byte(prolog)
+ for _, item := range table {
+ data = append(data, fmt.Sprintf("# %s\n%s=%s\n",
+ item.description, item.key, item.def)...)
+ }
+ return writeConfigurationFile(pathHint, data)
+}
+
+/// Convenience wrapper suitable for most simple applications.
+func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
+ prologLines := []string{
+ `# ` + projectName + ` ` + projectVersion + ` configuration file`,
+ "#",
+ `# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}`,
+ `# /` + projectName + ` as well as in $XDG_CONFIG_DIRS/` + projectName,
+ ``,
+ ``,
+ }
+
+ path, err := simpleConfigWriteDefault(
+ pathHint, strings.Join(prologLines, "\n"), table)
+ if err != nil {
+ exitFatal("%s", err)
+ }
+
+ printStatus("configuration written to `%s'", path)
+}
+
+// --- Configuration -----------------------------------------------------------
+
+var configTable = []simpleConfigItem{
+ {"server_name", "", "Server name"},
+ {"server_info", "My server", "Brief server description"},
+ {"motd", "", "MOTD filename"},
+ {"catalog", "", "Localisation catalog"},
+
+ {"bind", ":6667", "Bind addresses of the IRC server"},
+ {"tls_cert", "", "Server TLS certificate (PEM)"},
+ {"tls_key", "", "Server TLS private key (PEM)"},
+
+ {"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},
+
+ {"max_connections", "0", "Global connection limit"},
+ {"ping_interval", "180", "Interval between PINGs (sec)"},
+}
+
+// --- Rate limiter ------------------------------------------------------------
+
+type floodDetector struct {
+ interval time.Duration // interval for the limit in seconds
+ limit uint // maximum number of events allowed
+ timestamps []time.Time // timestamps of last events
+ pos uint // index of the oldest event
+}
+
+func newFloodDetector(interval time.Duration, limit uint) *floodDetector {
+ return &floodDetector{
+ interval: interval,
+ limit: limit,
+ timestamps: make([]time.Time, limit+1),
+ pos: 0,
+ }
+}
+
+func (fd *floodDetector) check() bool {
+ now := time.Now()
+ fd.timestamps[fd.pos] = now
+
+ fd.pos++
+ if fd.pos > fd.limit {
+ fd.pos = 0
+ }
+
+ var count uint
+ begin := now.Add(-fd.interval)
+ for _, ts := range fd.timestamps {
+ if ts.After(begin) {
+ count++
+ }
+ }
+ return count <= fd.limit
+}
+
+// --- IRC protocol ------------------------------------------------------------
+
+//go:generate sh -c "./hid-gen-replies.sh > hid-replies.go < hid-replies"
+
+func ircToLower(c byte) byte {
+ switch c {
+ case '[':
+ return '{'
+ case ']':
+ return '}'
+ case '\\':
+ return '|'
+ case '~':
+ return '^'
+ }
+ if c >= 'A' && c <= 'Z' {
+ return c + ('a' - 'A')
+ }
+ return c
+}
+
+func ircToUpper(c byte) byte {
+ switch c {
+ case '{':
+ return '['
+ case '}':
+ return ']'
+ case '|':
+ return '\\'
+ case '^':
+ return '~'
+ }
+ if c >= 'a' && c <= 'z' {
+ return c - ('a' - 'A')
+ }
+ return c
+}
+
+// Convert identifier to a canonical form for case-insensitive comparisons.
+// ircToUpper is used so that statically initialized maps can be in uppercase.
+func ircToCanon(ident string) string {
+ var canon []byte
+ for _, c := range []byte(ident) {
+ canon = append(canon, ircToUpper(c))
+ }
+ return string(canon)
+}
+
+func ircEqual(s1, s2 string) bool {
+ return ircToCanon(s1) == ircToCanon(s2)
+}
+
+func ircFnmatch(pattern string, s string) bool {
+ pattern, s = ircToCanon(pattern), ircToCanon(s)
+ // FIXME: This should not support [] ranges and handle '/' specially.
+ // We could translate the pattern to a regular expression.
+ matched, _ := filepath.Match(pattern, s)
+ return matched
+}
+
+var reMsg = regexp.MustCompile(
+ `^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
+var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
+
+type message struct {
+ tags map[string]string // IRC 3.2 message tags
+ nick string // optional nickname
+ user string // optional username
+ host string // optional hostname or IP address
+ command string // command name
+ params []string // arguments
+}
+
+func ircUnescapeMessageTag(value string) string {
+ var buf []byte
+ escape := false
+ for i := 0; i < len(value); i++ {
+ if escape {
+ switch value[i] {
+ case ':':
+ buf = append(buf, ';')
+ case 's':
+ buf = append(buf, ' ')
+ case 'r':
+ buf = append(buf, '\r')
+ case 'n':
+ buf = append(buf, '\n')
+ default:
+ buf = append(buf, value[i])
+ }
+ escape = false
+ } else if value[i] == '\\' {
+ escape = true
+ } else {
+ buf = append(buf, value[i])
+ }
+ }
+ return string(buf)
+}
+
+func ircParseMessageTags(tags string, out map[string]string) {
+ for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
+ if equal := strings.IndexByte(tag, '='); equal < 0 {
+ out[tag] = ""
+ } else {
+ out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
+ }
+ }
+}
+
+func ircParseMessage(line string) *message {
+ m := reMsg.FindStringSubmatch(line)
+ if m == nil {
+ return nil
+ }
+
+ msg := message{nil, m[2], m[3], m[4], m[5], nil}
+ if m[1] != "" {
+ msg.tags = make(map[string]string)
+ ircParseMessageTags(m[1], msg.tags)
+ }
+ for _, x := range reArgs.FindAllString(m[6], -1) {
+ msg.params = append(msg.params, x[1:])
+ }
+ return &msg
+
+}
+
+// --- IRC token validation ----------------------------------------------------
+
+// Everything as per RFC 2812
+const (
+ ircMaxNickname = 9
+ ircMaxHostname = 63
+ ircMaxChannelName = 50
+ ircMaxMessageLength = 510
+)
+
+const (
+ reClassSpecial = "\\[\\]\\\\`_^{|}"
+ // "shortname" from RFC 2812 doesn't work how its author thought it would.
+ reShortname = "[0-9A-Za-z](-*[0-9A-Za-z])*"
+)
+
+var (
+ reHostname = regexp.MustCompile(
+ `^` + reShortname + `(\.` + reShortname + `)*$`)
+
+ // Extending ASCII to the whole range of Unicode letters.
+ reNickname = regexp.MustCompile(
+ `^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
+
+ // Notably, this won't match invalid UTF-8 characters, although the
+ // behaviour seems to be unstated in the documentation.
+ reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)
+
+ reChannelName = regexp.MustCompile(`^[^\0\007\r\n ,:]+$`)
+ reKey = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`)
+ reUserMask = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`)
+ reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
+)
+
+func ircValidateHostname(hostname string) error {
+ if hostname == "" {
+ return errors.New("the value is empty")
+ }
+ if !reHostname.MatchString(hostname) {
+ return errors.New("invalid format")
+ }
+ if len(hostname) > ircMaxHostname {
+ return errors.New("the value is too long")
+ }
+ return nil
+}
+
+func ircIsValidNickname(nickname string) bool {
+ return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
+}
+
+func ircIsValidUsername(username string) bool {
+ // XXX: We really should put a limit on this
+ // despite the RFC not mentioning one.
+ return reUsername.MatchString(username)
+}
+
+func ircIsValidChannelName(name string) bool {
+ return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
+}
+
+func ircIsValidKey(key string) bool {
+ // XXX: Should be 7-bit as well but whatever.
+ return reKey.MatchString(key)
+}
+
+func ircIsValidUserMask(mask string) bool {
+ return reUserMask.MatchString(mask)
+}
+
+func ircIsValidFingerprint(fp string) bool {
+ return reFingerprint.MatchString(fp)
+}
+
+// --- Clients (equals users) --------------------------------------------------
+
+type connCloseWriter interface {
+ net.Conn
+ CloseWrite() error
+}
+
+const ircSupportedUserModes = "aiwros"
+
+const (
+ ircUserModeInvisible uint = 1 << iota
+ ircUserModeRxWallops
+ ircUserModeRestricted
+ ircUserModeOperator
+ ircUserModeRxServerNotices
+)
+
+const (
+ ircCapMultiPrefix uint = 1 << iota
+ ircCapInviteNotify
+ ircCapEchoMessage
+ ircCapUserhostInNames
+ ircCapServerTime
+)
+
+type client struct {
+ transport net.Conn // underlying connection
+ tls *tls.Conn // TLS, if detected
+ conn connCloseWriter // high-level connection
+ recvQ []byte // unprocessed input
+ sendQ []byte // unprocessed output
+ reading bool // whether a reading goroutine is running
+ writing bool // whether a writing goroutine is running
+ closing bool // whether we're closing the connection
+ killTimer *time.Timer // hard kill timeout
+
+ opened time.Time // when the connection was opened
+ nSentMessages uint // number of sent messages total
+ sentBytes int // number of sent bytes total
+ nReceivedMessages uint // number of received messages total
+ receivedBytes int // number of received bytes total
+
+ hostname string // hostname or IP shown to the network
+ port string // port of the peer as a string
+ address string // full network address
+
+ pingTimer *time.Timer // we should send a PING
+ timeoutTimer *time.Timer // connection seems to be dead
+ registered bool // the user has registered
+ capNegotiating bool // negotiating capabilities
+ capsEnabled uint // enabled capabilities
+ capVersion uint // CAP protocol version
+
+ tlsCertFingerprint string // client certificate fingerprint
+
+ nickname string // IRC nickname (main identifier)
+ username string // IRC username
+ realname string // IRC realname (or e-mail)
+
+ mode uint // user's mode
+ awayMessage string // away message
+ lastActive time.Time // last PRIVMSG, to get idle time
+ invites map[string]bool // channel invitations by operators
+ antiflood *floodDetector // flood detector
+}
+
+// --- Channels ----------------------------------------------------------------
+
+const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
+
+const (
+ ircChanModeInviteOnly uint = 1 << iota
+ ircChanModeModerated
+ ircChanModeNoOutsideMsgs
+ ircChanModeQuiet
+ ircChanModePrivate
+ ircChanModeSecret
+ ircChanModeProtectedTopic
+
+ ircChanModeOperator
+ ircChanModeVoice
+)
+
+type channel struct {
+ name string // channel name
+ modes uint // channel modes
+ key string // channel key
+ userLimit int // user limit or -1
+ created time.Time // creation time
+
+ topic string // channel topic
+ topicWho string // who set the topic
+ topicTime time.Time // when the topic was set
+
+ userModes map[*client]uint // modes for all channel users
+
+ banList []string // ban list
+ exceptionList []string // exceptions from bans
+ inviteList []string // exceptions from +I
+}
+
+func (ch *channel) getMode(discloseSecrets bool) string {
+ var buf []byte
+ if 0 != ch.modes&ircChanModeInviteOnly {
+ buf = append(buf, 'i')
+ }
+ if 0 != ch.modes&ircChanModeModerated {
+ buf = append(buf, 'm')
+ }
+ if 0 != ch.modes&ircChanModeNoOutsideMsgs {
+ buf = append(buf, 'n')
+ }
+ if 0 != ch.modes&ircChanModeQuiet {
+ buf = append(buf, 'q')
+ }
+ if 0 != ch.modes&ircChanModePrivate {
+ buf = append(buf, 'p')
+ }
+ if 0 != ch.modes&ircChanModeSecret {
+ buf = append(buf, 's')
+ }
+ if 0 != ch.modes&ircChanModeProtectedTopic {
+ buf = append(buf, 'r')
+ }
+
+ if ch.userLimit != -1 {
+ buf = append(buf, 'l')
+ }
+ if ch.key != "" {
+ buf = append(buf, 'k')
+ }
+
+ // XXX: Is it correct to split it? Try it on an existing implementation.
+ if discloseSecrets {
+ if ch.userLimit != -1 {
+ buf = append(buf, fmt.Sprintf(" %d", ch.userLimit)...)
+ }
+ if ch.key != "" {
+ buf = append(buf, fmt.Sprintf(" %s", ch.key)...)
+ }
+ }
+ return string(buf)
+}
+
+// --- IRC server context ------------------------------------------------------
+
+type whowasInfo struct {
+ nickname, username, realname, hostname string
+}
+
+func newWhowasInfo(c *client) *whowasInfo {
+ return &whowasInfo{
+ nickname: c.nickname,
+ username: c.username,
+ realname: c.realname,
+ hostname: c.hostname,
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+type ircCommand struct {
+ requiresRegistration bool
+ handler func(*message, *client)
+
+ nReceived uint // number of commands received
+ bytesReceived int // number of bytes received total
+}
+
+type preparedEvent struct {
+ client *client
+ host string // client's hostname or literal IP address
+ isTLS bool // the client seems to use TLS
+}
+
+type readEvent struct {
+ client *client
+ data []byte // new data from the client
+ err error // read error
+}
+
+type writeEvent struct {
+ client *client
+ written int // amount of bytes written
+ err error // write error
+}
+
+// TODO: Maybe we want to keep it in a struct?
+// A better question might be: can we run multiple instances of it?
+var (
+ // network
+
+ listeners []net.Listener
+ clients = make(map[*client]bool)
+
+ // IRC state
+
+ // XXX: Beware that maps with identifier keys need to be indexed correctly.
+ // We might want to enforce accessor functions for users and channels.
+
+ started time.Time // when the server has been started
+ users = make(map[string]*client) // maps nicknames to clients
+ channels = make(map[string]*channel) // maps channel names to data
+ whowas = make(map[string]*whowasInfo) // WHOWAS registry
+
+ // event loop
+
+ quitting bool
+ quitTimer <-chan time.Time
+
+ sigs = make(chan os.Signal, 1)
+ conns = make(chan net.Conn)
+ prepared = make(chan preparedEvent)
+ reads = make(chan readEvent)
+ writes = make(chan writeEvent)
+ timers = make(chan func())
+
+ // configuration
+
+ config simpleConfig // server configuration
+ tlsConf *tls.Config // TLS connection configuration
+ serverName string // our server name
+ pingInterval time.Duration // ping interval
+ maxConnections int // max connections allowed or 0
+ motd []string // MOTD (none if empty)
+ catalog map[int]string // message catalog for server msgs
+ operators map[string]bool // TLS certificate fingerprints for IRCops
+)
+
+// Forcefully tear down all connections.
+func forceQuit(reason string) {
+ if !quitting {
+ exitFatal("forceQuit called without initiateQuit")
+ }
+
+ printStatus("forced shutdown (%s)", reason)
+ for c := range clients {
+ // initiateQuit has already unregistered the client.
+ c.kill("Shutting down")
+ }
+}
+
+// Initiate a clean shutdown of the whole daemon.
+func initiateQuit() {
+ printStatus("shutting down")
+ for _, ln := range listeners {
+ if err := ln.Close(); err != nil {
+ printError("%s", err)
+ }
+ }
+ for c := range clients {
+ c.closeLink("Shutting down")
+ }
+
+ quitTimer = time.After(5 * time.Second)
+ quitting = true
+}
+
+func ircChannelCreate(name string) *channel {
+ ch := &channel{
+ name: name,
+ userLimit: -1,
+ created: time.Now(),
+ userModes: make(map[*client]uint),
+ }
+ channels[ircToCanon(name)] = ch
+ return ch
+}
+
+func ircChannelDestroyIfEmpty(ch *channel) {
+ if len(ch.userModes) == 0 {
+ delete(channels, ircToCanon(ch.name))
+ }
+}
+
+func ircNotifyRoommates(c *client, message string) {
+ targets := make(map[*client]bool)
+ for _, ch := range channels {
+ _, present := ch.userModes[c]
+ if !present || 0 != ch.modes&ircChanModeQuiet {
+ continue
+ }
+ for client := range ch.userModes {
+ targets[client] = true
+ }
+ }
+
+ for roommate := range targets {
+ if roommate != c {
+ roommate.send(message)
+ }
+ }
+}
+
+// --- 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')
+ }
+ if 0 != m&ircUserModeRxWallops {
+ mode = append(mode, 'w')
+ }
+ if 0 != m&ircUserModeRestricted {
+ mode = append(mode, 'r')
+ }
+ if 0 != m&ircUserModeOperator {
+ mode = append(mode, 'o')
+ }
+ if 0 != m&ircUserModeRxServerNotices {
+ mode = append(mode, 's')
+ }
+ return mode
+}
+
+func (c *client) getMode() string {
+ var mode []byte
+ if c.awayMessage != "" {
+ mode = append(mode, 'a')
+ }
+ return string(ircAppendClientModes(c.mode, mode))
+}
+
+func (c *client) send(line string) {
+ if c.conn == nil || c.closing {
+ return
+ }
+
+ oldSendQLen := len(c.sendQ)
+
+ // So far there's only one message tag we use, so we can do it simple;
+ // note that a 1024-character limit applies to messages with tags on.
+ if 0 != c.capsEnabled&ircCapServerTime {
+ c.sendQ = time.Now().UTC().
+ AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
+ }
+
+ bytes := []byte(line)
+ if len(bytes) > ircMaxMessageLength {
+ 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"...)
+ c.flushSendQ()
+
+ // Technically we haven't sent it yet but that's a minor detail
+ c.nSentMessages++
+ c.sentBytes += len(c.sendQ) - oldSendQLen
+}
+
+func (c *client) sendf(format string, a ...interface{}) {
+ c.send(fmt.Sprintf(format, a...))
+}
+
+func (c *client) addToWhowas() {
+ // Only keeping one entry for each nickname.
+ // TODO: Make sure this list doesn't get too long, for example by
+ // putting them in a linked list ordered by time.
+ whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
+}
+
+func (c *client) nicknameOrStar() string {
+ if c.nickname == "" {
+ return "*"
+ }
+ return c.nickname
+}
+
+func (c *client) unregister(reason string) {
+ if !c.registered {
+ return
+ }
+
+ ircNotifyRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
+ c.nickname, c.username, c.hostname, reason))
+
+ // The QUIT message will take care of state on clients.
+ for _, ch := range channels {
+ delete(ch.userModes, c)
+ ircChannelDestroyIfEmpty(ch)
+ }
+
+ c.addToWhowas()
+ delete(users, ircToCanon(c.nickname))
+ c.nickname = ""
+ c.registered = false
+}
+
+// Close the connection and forget about the client.
+func (c *client) kill(reason string) {
+ if reason == "" {
+ c.unregister("Client exited")
+ } else {
+ c.unregister(reason)
+ }
+
+ c.printDebug("client destroyed (%s)", reason)
+
+ // Try to send a "close notify" alert if the TLS object is ready,
+ // otherwise just tear down the transport.
+ if c.conn != nil {
+ _ = c.conn.Close()
+ } else {
+ _ = c.transport.Close()
+ }
+
+ c.cancelTimers()
+ delete(clients, c)
+}
+
+// Tear down the client connection, trying to do so in a graceful manner.
+func (c *client) closeLink(reason string) {
+ // Let's just cut the connection, the client can try again later.
+ // We also want to avoid accidentally writing to the socket before
+ // address resolution has finished.
+ if c.conn == nil {
+ c.kill(reason)
+ return
+ }
+ if c.closing {
+ return
+ }
+
+ // We push an "ERROR" message to the write buffer and let the writer send
+ // it, with some arbitrary timeout. The "closing" state makes sure
+ // that a/ we ignore any successive messages, and b/ that the connection
+ // is killed after the write buffer is transferred and emptied.
+ // (Since we send this message, we don't need to call CloseWrite here.)
+ c.sendf("ERROR :Closing link: %s[%s] (%s)",
+ c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
+ c.closing = true
+
+ c.unregister(reason)
+ c.setKillTimer()
+}
+
+func (c *client) inMaskList(masks []string) bool {
+ client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
+ for _, mask := range masks {
+ if ircFnmatch(mask, client) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *client) getTLSCertFingerprint() string {
+ if c.tls == nil {
+ return ""
+ }
+
+ peerCerts := c.tls.ConnectionState().PeerCertificates
+ if len(peerCerts) == 0 {
+ return ""
+ }
+
+ hash := sha256.Sum256(peerCerts[0].Raw)
+ return hex.EncodeToString(hash[:])
+}
+
+// --- Timers ------------------------------------------------------------------
+
+// Free the resources of timers that haven't fired yet and for timers that are
+// in between firing and being collected by the event loop, mark that the event
+// should not be acted upon.
+func (c *client) cancelTimers() {
+ for _, timer := range []**time.Timer{
+ &c.killTimer, &c.timeoutTimer, &c.pingTimer,
+ } {
+ if *timer != nil {
+ (*timer).Stop()
+ *timer = nil
+ }
+ }
+}
+
+// Arrange for a function to be called later from the main goroutine.
+func (c *client) setTimer(timer **time.Timer, delay time.Duration, cb func()) {
+ c.cancelTimers()
+
+ var identityCapture *time.Timer
+ identityCapture = time.AfterFunc(delay, func() {
+ timers <- func() {
+ // The timer might have been cancelled or even replaced.
+ // When the client is killed, this will be nil.
+ if *timer == identityCapture {
+ cb()
+ }
+ }
+ })
+
+ *timer = identityCapture
+}
+
+func (c *client) setKillTimer() {
+ c.setTimer(&c.killTimer, pingInterval, func() {
+ c.kill("Timeout")
+ })
+}
+
+func (c *client) setTimeoutTimer() {
+ c.setTimer(&c.timeoutTimer, pingInterval, func() {
+ c.closeLink(fmt.Sprintf("Ping timeout: >%d seconds",
+ pingInterval/time.Second))
+ })
+}
+
+func (c *client) setPingTimer() {
+ c.setTimer(&c.pingTimer, pingInterval, func() {
+ c.sendf("PING :%s", serverName)
+ c.setTimeoutTimer()
+ })
+}
+
+// --- IRC command handling ----------------------------------------------------
+
+func (c *client) makeReply(id int, ap ...interface{}) string {
+ s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
+ if reply, ok := catalog[id]; ok {
+ return s + fmt.Sprintf(reply, ap...)
+ }
+ return s + fmt.Sprintf(defaultReplies[id], ap...)
+}
+
+// XXX: This way simple static analysis cannot typecheck the arguments, so we
+// need to be careful.
+func (c *client) sendReply(id int, args ...interface{}) {
+ c.send(c.makeReply(id, args...))
+}
+
+/// Send a space-separated list of words across as many replies as needed.
+func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
+ common := c.makeReply(id, args...)
+
+ // We always send at least one message (there might be a client that
+ // expects us to send this message at least once).
+ if len(items) == 0 {
+ items = append(items, "")
+ }
+
+ for len(items) > 0 {
+ // If not even a single item fits in the limit (which may happen,
+ // in theory) it just gets cropped. We could also skip it.
+ reply := append([]byte(common), items[0]...)
+ items = items[1:]
+
+ // Append as many items as fits in a single message.
+ for len(items) > 0 &&
+ len(reply)+1+len(items[0]) <= ircMaxMessageLength {
+ reply = append(reply, ' ')
+ reply = append(reply, items[0]...)
+ items = items[1:]
+ }
+
+ c.send(string(reply))
+ }
+}
+
+func (c *client) sendMOTD() {
+ if len(motd) == 0 {
+ c.sendReply(ERR_NOMOTD)
+ return
+ }
+
+ c.sendReply(RPL_MOTDSTART, serverName)
+ for _, line := range motd {
+ c.sendReply(RPL_MOTD, line)
+ }
+ c.sendReply(RPL_ENDOFMOTD)
+}
+
+func (c *client) sendLUSERS() {
+ nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
+ for c := range clients {
+ if c.registered {
+ nUsers++
+ } else {
+ nUnknown++
+ }
+ if 0 != c.mode&ircUserModeOperator {
+ nOpers++
+ }
+ }
+
+ nChannels := 0
+ for _, ch := range channels {
+ if 0 != ch.modes&ircChanModeSecret {
+ nChannels++
+ }
+ }
+
+ c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
+ if nOpers != 0 {
+ c.sendReply(RPL_LUSEROP, nOpers)
+ }
+ if nUnknown != 0 {
+ c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
+ }
+ if nChannels != 0 {
+ c.sendReply(RPL_LUSERCHANNELS, nChannels)
+ }
+ c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
+}
+
+func ircIsThisMe(target string) bool {
+ // Target servers can also be matched by their users
+ if ircFnmatch(target, serverName) {
+ return true
+ }
+ _, ok := users[ircToCanon(target)]
+ return ok
+}
+
+func (c *client) sendISUPPORT() {
+ // Only # channels, +e supported, +I supported, unlimited arguments to MODE.
+ c.sendReply(RPL_ISUPPORT, fmt.Sprintf("CHANTYPES=# EXCEPTS INVEX MODES"+
+ " TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
+ " NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName))
+}
+
+func (c *client) tryFinishRegistration() {
+ if c.registered || c.capNegotiating {
+ return
+ }
+ if c.nickname == "" || c.username == "" {
+ return
+ }
+
+ c.registered = true
+ c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
+
+ c.sendReply(RPL_YOURHOST, serverName, projectVersion)
+ // The purpose of this message eludes me.
+ c.sendReply(RPL_CREATED, started.Format("Mon, 02 Jan 2006"))
+ c.sendReply(RPL_MYINFO, serverName, projectVersion,
+ ircSupportedUserModes, ircSupportedChanModes)
+
+ c.sendISUPPORT()
+ c.sendLUSERS()
+ c.sendMOTD()
+
+ if mode := c.getMode(); mode != "" {
+ c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
+ }
+
+ c.tlsCertFingerprint = c.getTLSCertFingerprint()
+ if c.tlsCertFingerprint != "" {
+ c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
+ serverName, c.nickname, c.tlsCertFingerprint)
+ }
+
+ delete(whowas, ircToCanon(c.nickname))
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// IRCv3 capability negotiation. See http://ircv3.org for details.
+
+type ircCapArgs struct {
+ subcommand string // the subcommand being processed
+ fullParams string // whole parameter string
+ params []string // split parameters
+ target string // target parameter for replies
+}
+
+var ircCapTable = []struct {
+ flag uint // flag
+ name string // name of the capability
+}{
+ {ircCapMultiPrefix, "multi-prefix"},
+ {ircCapInviteNotify, "invite-notify"},
+ {ircCapEchoMessage, "echo-message"},
+ {ircCapUserhostInNames, "userhost-in-names"},
+ {ircCapServerTime, "server-time"},
+}
+
+func (c *client) handleCAPLS(a *ircCapArgs) {
+ if len(a.params) == 1 {
+ if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "Ignoring invalid protocol version number")
+ } else {
+ c.capVersion = uint(ver)
+ }
+ }
+
+ c.capNegotiating = true
+ c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
+ " userhost-in-names server-time", serverName, a.target)
+}
+
+func (c *client) handleCAPLIST(a *ircCapArgs) {
+ caps := []string{}
+ for _, cap := range ircCapTable {
+ if 0 != c.capsEnabled&cap.flag {
+ caps = append(caps, cap.name)
+ }
+ }
+
+ c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
+ strings.Join(caps, " "))
+}
+
+func ircDecodeCapability(name string) uint {
+ for _, cap := range ircCapTable {
+ if cap.name == name {
+ return cap.flag
+ }
+ }
+ return 0
+}
+
+func (c *client) handleCAPREQ(a *ircCapArgs) {
+ c.capNegotiating = true
+
+ newCaps := c.capsEnabled
+ ok := true
+ for _, param := range a.params {
+ removing := false
+ name := param
+ if name[:1] == "-" {
+ removing = true
+ name = name[1:]
+ }
+
+ if cap := ircDecodeCapability(name); cap == 0 {
+ ok = false
+ } else if removing {
+ newCaps &= ^cap
+ } else {
+ newCaps |= cap
+ }
+ }
+
+ if ok {
+ c.capsEnabled = newCaps
+ c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
+ } else {
+ c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
+ }
+}
+
+func (c *client) handleCAPACK(a *ircCapArgs) {
+ if len(a.params) > 0 {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "No acknowledgable capabilities supported")
+ }
+}
+
+func (c *client) handleCAPEND(a *ircCapArgs) {
+ c.capNegotiating = false
+ c.tryFinishRegistration()
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+var ircCapHandlers = map[string]func(*client, *ircCapArgs){
+ "LS": (*client).handleCAPLS,
+ "LIST": (*client).handleCAPLIST,
+ "REQ": (*client).handleCAPREQ,
+ "ACK": (*client).handleCAPACK,
+ "END": (*client).handleCAPEND,
+}
+
+// XXX: Maybe these also deserve to be methods for client? They operate on
+// global state, though.
+
+func ircHandleCAP(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ args := &ircCapArgs{
+ target: c.nicknameOrStar(),
+ subcommand: msg.params[0],
+ fullParams: "",
+ params: []string{},
+ }
+
+ if len(msg.params) > 1 {
+ args.fullParams = msg.params[1]
+ args.params = splitString(args.fullParams, " ", true)
+ }
+
+ if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
+ c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
+ "Invalid CAP subcommand")
+ } else {
+ fn(c, args)
+ }
+}
+
+func ircHandlePASS(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ }
+
+ // We have TLS client certificates for this purpose; ignoring.
+}
+
+func ircHandleNICK(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NONICKNAMEGIVEN)
+ return
+ }
+
+ nickname := msg.params[0]
+ if !ircIsValidNickname(nickname) {
+ c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
+ return
+ }
+
+ nicknameCanon := ircToCanon(nickname)
+ if client, ok := users[nicknameCanon]; ok && client != c {
+ c.sendReply(ERR_NICKNAMEINUSE, nickname)
+ return
+ }
+
+ if c.registered {
+ c.addToWhowas()
+
+ message := fmt.Sprintf(":%s!%s@%s NICK :%s",
+ c.nickname, c.username, c.hostname, nickname)
+ ircNotifyRoommates(c, message)
+ c.send(message)
+ }
+
+ // Release the old nickname and allocate a new one.
+ if c.nickname != "" {
+ delete(users, ircToCanon(c.nickname))
+ }
+
+ c.nickname = nickname
+ users[nicknameCanon] = c
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSER(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ return
+ }
+ if len(msg.params) < 4 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
+
+ // Unfortunately, the protocol doesn't give us any means of rejecting it.
+ if !ircIsValidUsername(username) {
+ username = "*"
+ }
+
+ c.username = username
+ c.realname = realname
+
+ c.mode = 0
+ if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
+ if 0 != m&4 {
+ c.mode |= ircUserModeRxWallops
+ }
+ if 0 != m&8 {
+ c.mode |= ircUserModeInvisible
+ }
+ }
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSERHOST(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ 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) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendLUSERS()
+}
+
+func ircHandleMOTD(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendMOTD()
+}
+
+func ircHandlePING(msg *message, c *client) {
+ // XXX: The RFC is pretty incomprehensible about the exact usage.
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ } else {
+ c.sendf(":%s PONG :%s", serverName, msg.params[0])
+ }
+}
+
+func ircHandlePONG(msg *message, c *client) {
+ // We are the only server, so we don't have to care too much.
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ return
+ }
+
+ // Set a new timer to send another PING
+ c.setPingTimer()
+}
+
+func ircHandleQUIT(msg *message, c *client) {
+ reason := c.nickname
+ if len(msg.params) > 0 {
+ reason = msg.params[0]
+ }
+
+ c.closeLink("Quit: " + reason)
+}
+
+func ircHandleTIME(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ c.sendReply(RPL_TIME, serverName,
+ time.Now().Format("Mon Jan _2 2006 15:04:05"))
+}
+
+func ircHandleVERSION(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ postVersion := 0
+ if debugMode {
+ postVersion = 1
+ }
+
+ c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
+ projectName+" "+projectVersion)
+ c.sendISUPPORT()
+}
+
+func ircChannelMulticast(ch *channel, msg string, except *client) {
+ for c := range ch.userModes {
+ if c != except {
+ c.send(msg)
+ }
+ }
+}
+
+func ircModifyMode(mask *uint, mode uint, add bool) bool {
+ orig := *mask
+ if add {
+ *mask |= mode
+ } else {
+ *mask &= ^mode
+ }
+ return *mask != orig
+}
+
+func ircUpdateUserMode(c *client, newMode uint) {
+ oldMode := c.mode
+ c.mode = newMode
+
+ added, removed := newMode & ^oldMode, oldMode & ^newMode
+
+ var diff []byte
+ if added != 0 {
+ diff = append(diff, '+')
+ diff = ircAppendClientModes(added, diff)
+ }
+ if removed != 0 {
+ diff = append(diff, '-')
+ diff = ircAppendClientModes(removed, diff)
+ }
+
+ if len(diff) > 0 {
+ c.sendf(":%s MODE %s :%s", c.nickname, c.nickname, string(diff))
+ }
+}
+
+func ircHandleUserModeChange(c *client, modeString string) {
+ newMode := c.mode
+ adding := true
+
+ for _, flag := range modeString {
+ switch flag {
+ case '+':
+ adding = true
+ case '-':
+ adding = false
+
+ case 'a':
+ // Ignore, the client should use AWAY.
+ case 'i':
+ ircModifyMode(&newMode, ircUserModeInvisible, adding)
+ case 'w':
+ ircModifyMode(&newMode, ircUserModeRxWallops, adding)
+ case 'r':
+ // It's not possible ot un-restrict yourself.
+ if adding {
+ newMode |= ircUserModeRestricted
+ }
+ case 'o':
+ if !adding {
+ newMode &= ^ircUserModeOperator
+ } else if operators[c.tlsCertFingerprint] {
+ newMode |= ircUserModeOperator
+ } else {
+ c.sendf(":%s NOTICE %s :Either you're not using an TLS"+
+ " client certificate, or the fingerprint doesn't match",
+ serverName, c.nickname)
+ }
+ case 's':
+ ircModifyMode(&newMode, ircUserModeRxServerNotices, adding)
+ default:
+ c.sendReply(ERR_UMODEUNKNOWNFLAG)
+ return
+ }
+ }
+ ircUpdateUserMode(c, newMode)
+}
+
+func ircSendChannelList(c *client, channelName string, list []string,
+ reply, endReply int) {
+ for _, line := range list {
+ c.sendReply(reply, channelName, line)
+ }
+ c.sendReply(endReply, channelName)
+}
+
+func ircCheckExpandUserMask(mask string) string {
+ var result []byte
+ result = append(result, mask...)
+
+ // Make sure it is a complete mask.
+ if bytes.IndexByte(result, '!') < 0 {
+ result = append(result, "!*"...)
+ }
+ if bytes.IndexByte(result, '@') < 0 {
+ result = append(result, "@*"...)
+ }
+
+ // And validate whatever the result is.
+ s := string(result)
+ if !ircIsValidUserMask(s) {
+ return ""
+ }
+
+ return s
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Channel MODE command handling. This is by far the worst command to implement
+// from the whole RFC; don't blame me if it doesn't work exactly as expected.
+
+type modeProcessor struct {
+ params []string // mode string parameters
+
+ c *client // who does the changes
+ ch *channel // the channel we're modifying
+ present bool // c present on ch
+ modes uint // channel user modes
+
+ adding bool // currently adding modes
+ modeChar byte // currently processed mode char
+
+ added []byte // added modes
+ removed []byte // removed modes
+ output *[]byte // "added" or "removed"
+
+ addedParams []string // params for added modes
+ removedParams []string // params for removed modes
+ outputParams *[]string // "addedParams" or "removedParams"
+}
+
+func (mp *modeProcessor) nextParam() string {
+ if len(mp.params) == 0 {
+ return ""
+ }
+
+ param := mp.params[0]
+ mp.params = mp.params[1:]
+ return param
+}
+
+func (mp *modeProcessor) checkOperator() bool {
+ if (mp.present && 0 != mp.modes&ircChanModeOperator) ||
+ 0 != mp.c.mode&ircUserModeOperator {
+ return true
+ }
+
+ mp.c.sendReply(ERR_CHANOPRIVSNEEDED, mp.ch.name)
+ return false
+}
+
+func (mp *modeProcessor) doUser(mode uint) {
+ target := mp.nextParam()
+ if !mp.checkOperator() || target == "" {
+ return
+ }
+
+ if client := users[ircToCanon(target)]; client == nil {
+ mp.c.sendReply(ERR_NOSUCHNICK, target)
+ } else if modes, present := mp.ch.userModes[client]; !present {
+ mp.c.sendReply(ERR_USERNOTINCHANNEL, target, mp.ch.name)
+ } else if ircModifyMode(&modes, mode, mp.adding) {
+ mp.ch.userModes[client] = modes
+ *mp.output = append(*mp.output, mp.modeChar)
+ *mp.outputParams = append(*mp.outputParams, client.nickname)
+ }
+}
+
+func (mp *modeProcessor) doChan(mode uint) bool {
+ if !mp.checkOperator() || !ircModifyMode(&mp.ch.modes, mode, mp.adding) {
+ return false
+ }
+ *mp.output = append(*mp.output, mp.modeChar)
+ return true
+}
+
+func (mp *modeProcessor) doChanRemove(modeChar byte, mode uint) {
+ if mp.adding && ircModifyMode(&mp.ch.modes, mode, false) {
+ mp.removed = append(mp.removed, modeChar)
+ }
+}
+
+func (mp *modeProcessor) doList(list *[]string, listMsg, endMsg int) {
+ target := mp.nextParam()
+ if target == "" {
+ if mp.adding {
+ ircSendChannelList(mp.c, mp.ch.name, *list, listMsg, endMsg)
+ }
+ return
+ }
+
+ if !mp.checkOperator() {
+ return
+ }
+
+ mask := ircCheckExpandUserMask(target)
+ if mask == "" {
+ return
+ }
+
+ var i int
+ for i = 0; i < len(*list); i++ {
+ if ircEqual((*list)[i], mask) {
+ break
+ }
+ }
+
+ found := i < len(*list)
+ if found != mp.adding {
+ if mp.adding {
+ *list = append(*list, mask)
+ } else {
+ *list = append((*list)[:i], (*list)[i+1:]...)
+ }
+
+ *mp.output = append(*mp.output, mp.modeChar)
+ *mp.outputParams = append(*mp.outputParams, mask)
+ }
+}
+
+func (mp *modeProcessor) doKey() {
+ target := mp.nextParam()
+ if !mp.checkOperator() || target == "" {
+ return
+ }
+
+ if !mp.adding {
+ if mp.ch.key == "" || !ircEqual(target, mp.ch.key) {
+ return
+ }
+
+ mp.removed = append(mp.removed, mp.modeChar)
+ mp.removedParams = append(mp.removedParams, mp.ch.key)
+ mp.ch.key = ""
+ } else if !ircIsValidKey(target) {
+ // TODO: We should notify the user somehow.
+ return
+ } else if mp.ch.key != "" {
+ mp.c.sendReply(ERR_KEYSET, mp.ch.name)
+ } else {
+ mp.ch.key = target
+ mp.added = append(mp.added, mp.modeChar)
+ mp.addedParams = append(mp.addedParams, mp.ch.key)
+ }
+}
+
+func (mp *modeProcessor) doLimit() {
+ if !mp.checkOperator() {
+ return
+ }
+
+ if !mp.adding {
+ if mp.ch.userLimit == -1 {
+ return
+ }
+
+ mp.ch.userLimit = -1
+ mp.removed = append(mp.removed, mp.modeChar)
+ } else if target := mp.nextParam(); target != "" {
+ if x, err := strconv.ParseInt(target, 10, 32); err == nil && x > 0 {
+ mp.ch.userLimit = int(x)
+ mp.added = append(mp.added, mp.modeChar)
+ mp.addedParams = append(mp.addedParams, target)
+ }
+ }
+}
+
+func (mp *modeProcessor) step(modeChar byte) bool {
+ mp.modeChar = modeChar
+ switch mp.modeChar {
+ case '+':
+ mp.adding = true
+ mp.output = &mp.added
+ mp.outputParams = &mp.addedParams
+ case '-':
+ mp.adding = false
+ mp.output = &mp.removed
+ mp.outputParams = &mp.removedParams
+
+ case 'o':
+ mp.doUser(ircChanModeOperator)
+ case 'v':
+ mp.doUser(ircChanModeVoice)
+
+ case 'i':
+ mp.doChan(ircChanModeInviteOnly)
+ case 'm':
+ mp.doChan(ircChanModeModerated)
+ case 'n':
+ mp.doChan(ircChanModeNoOutsideMsgs)
+ case 'q':
+ mp.doChan(ircChanModeQuiet)
+ case 't':
+ mp.doChan(ircChanModeProtectedTopic)
+
+ case 'p':
+ if mp.doChan(ircChanModePrivate) {
+ mp.doChanRemove('s', ircChanModeSecret)
+ }
+ case 's':
+ if mp.doChan(ircChanModeSecret) {
+ mp.doChanRemove('p', ircChanModePrivate)
+ }
+
+ case 'b':
+ mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST)
+ case 'e':
+ mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
+ case 'I':
+ mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST)
+
+ case 'k':
+ mp.doKey()
+ case 'l':
+ mp.doLimit()
+
+ default:
+ // It's not safe to continue, results could be undesired.
+ mp.c.sendReply(ERR_UNKNOWNMODE, modeChar, mp.ch.name)
+ return false
+ }
+ return true
+}
+
+func ircHandleChanModeChange(c *client, ch *channel, params []string) {
+ modes, present := ch.userModes[c]
+ mp := &modeProcessor{
+ c: c,
+ ch: ch,
+ present: present,
+ modes: modes,
+ params: params,
+ }
+
+Outer:
+ for {
+ modeString := mp.nextParam()
+ if modeString == "" {
+ break
+ }
+
+ mp.step('+')
+ for _, modeChar := range []byte(modeString) {
+ if !mp.step(modeChar) {
+ break Outer
+ }
+ }
+ }
+
+ // TODO: Limit to three changes with parameter per command.
+ if len(mp.added) > 0 || len(mp.removed) > 0 {
+ buf := []byte(fmt.Sprintf(":%s!%s@%s MODE %s ",
+ mp.c.nickname, mp.c.username, mp.c.hostname, mp.ch.name))
+ if len(mp.added) > 0 {
+ buf = append(buf, '+')
+ buf = append(buf, mp.added...)
+ }
+ if len(mp.removed) > 0 {
+ buf = append(buf, '-')
+ buf = append(buf, mp.removed...)
+ }
+ for _, param := range mp.addedParams {
+ buf = append(buf, ' ')
+ buf = append(buf, param...)
+ }
+ for _, param := range mp.removedParams {
+ buf = append(buf, ' ')
+ buf = append(buf, param...)
+ }
+ ircChannelMulticast(mp.ch, string(buf), nil)
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func ircHandleMODE(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ target := msg.params[0]
+ client := users[ircToCanon(target)]
+ ch := channels[ircToCanon(target)]
+
+ if client != nil {
+ if !ircEqual(target, c.nickname) {
+ c.sendReply(ERR_USERSDONTMATCH)
+ return
+ }
+
+ if len(msg.params) < 2 {
+ c.sendReply(RPL_UMODEIS, c.getMode())
+ } else {
+ ircHandleUserModeChange(c, msg.params[1])
+ }
+ } else if ch != nil {
+ if len(msg.params) < 2 {
+ _, present := ch.userModes[c]
+ c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present))
+ c.sendReply(RPL_CREATIONTIME, target, ch.created.Unix())
+ } else {
+ ircHandleChanModeChange(c, ch, msg.params[1:])
+ }
+ } else {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ }
+}
+
+func ircHandleUserMessage(msg *message, c *client,
+ command string, allowAwayReply bool) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NORECIPIENT, msg.command)
+ return
+ }
+ if len(msg.params) < 2 || msg.params[1] == "" {
+ c.sendReply(ERR_NOTEXTTOSEND)
+ return
+ }
+
+ target, text := msg.params[0], msg.params[1]
+ message := fmt.Sprintf(":%s!%s@%s %s %s :%s",
+ c.nickname, c.username, c.hostname, command, target, text)
+
+ if client := users[ircToCanon(target)]; client != nil {
+ client.send(message)
+ if allowAwayReply && client.awayMessage != "" {
+ c.sendReply(RPL_AWAY, target, client.awayMessage)
+ }
+
+ // Acknowledging a message from the client to itself would be silly.
+ if client != c && (0 != c.capsEnabled&ircCapEchoMessage) {
+ c.send(message)
+ }
+ } else if ch := channels[ircToCanon(target)]; ch != nil {
+ modes, present := ch.userModes[c]
+
+ outsider := !present && 0 != ch.modes&ircChanModeNoOutsideMsgs
+ moderated := 0 != ch.modes&ircChanModeModerated &&
+ 0 == modes&(ircChanModeVoice|ircChanModeOperator)
+ banned := c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList)
+
+ if outsider || moderated || banned {
+ c.sendReply(ERR_CANNOTSENDTOCHAN, target)
+ return
+ }
+
+ except := c
+ if 0 != c.capsEnabled&ircCapEchoMessage {
+ except = nil
+ }
+
+ ircChannelMulticast(ch, message, except)
+ } else {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ }
+}
+
+func ircHandlePRIVMSG(msg *message, c *client) {
+ ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */)
+ c.lastActive = time.Now()
+}
+
+func ircHandleNOTICE(msg *message, c *client) {
+ ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */)
+}
+
+func ircHandleLIST(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+
+ // XXX: Maybe we should skip ircUserModeInvisible from user counts.
+ if len(msg.params) == 0 {
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
+ c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
+ }
+ }
+ } else {
+ for _, target := range splitString(msg.params[0], ",", true) {
+ if ch := channels[ircToCanon(target)]; ch != nil &&
+ 0 == ch.modes&ircChanModeSecret {
+ c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
+ }
+ }
+ }
+ c.sendReply(RPL_LISTEND)
+}
+
+func ircAppendPrefixes(c *client, modes uint, buf []byte) []byte {
+ var all []byte
+ if 0 != modes&ircChanModeOperator {
+ all = append(all, '@')
+ }
+ if 0 != modes&ircChanModeVoice {
+ all = append(all, '+')
+ }
+
+ if len(all) > 0 {
+ if 0 != c.capsEnabled&ircCapMultiPrefix {
+ buf = append(buf, all...)
+ } else {
+ buf = append(buf, all[0])
+ }
+ }
+ return buf
+}
+
+func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string {
+ result := string(ircAppendPrefixes(c, modes, nil)) + target.nickname
+ if 0 != c.capsEnabled&ircCapUserhostInNames {
+ result += fmt.Sprintf("!%s@%s", target.username, target.hostname)
+ }
+ return result
+}
+
+func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[*client]bool) {
+ kind := '='
+ if 0 != ch.modes&ircChanModeSecret {
+ kind = '@'
+ } else if 0 != ch.modes&ircChanModePrivate {
+ kind = '*'
+ }
+
+ _, present := ch.userModes[c]
+
+ var nicks []string
+ for client, modes := range ch.userModes {
+ if !present && 0 != client.mode&ircUserModeInvisible {
+ continue
+ }
+ if usedNicks != nil {
+ usedNicks[client] = true
+ }
+ nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes))
+ }
+ c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "")
+}
+
+func ircSendDisassociatedNames(c *client, usedNicks map[*client]bool) {
+ var nicks []string
+ for _, client := range users {
+ if 0 == client.mode&ircUserModeInvisible && !usedNicks[client] {
+ nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0))
+ }
+ }
+ if len(nicks) > 0 {
+ c.sendReplyVector(RPL_NAMREPLY, nicks, '*', "*", "")
+ }
+}
+
+func ircHandleNAMES(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+
+ if len(msg.params) == 0 {
+ usedNicks := make(map[*client]bool)
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
+ ircSendRPLNAMREPLY(c, ch, usedNicks)
+ }
+ }
+
+ // Also send all visible users we haven't listed yet.
+ ircSendDisassociatedNames(c, usedNicks)
+ c.sendReply(RPL_ENDOFNAMES, "*")
+ } else {
+ for _, target := range splitString(msg.params[0], ",", true) {
+ if ch := channels[ircToCanon(target)]; ch == nil {
+ } else if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&ircChanModeSecret {
+ ircSendRPLNAMREPLY(c, ch, nil)
+ c.sendReply(RPL_ENDOFNAMES, target)
+ }
+ }
+ }
+}
+
+func ircSendRPLWHOREPLY(c *client, ch *channel, target *client) {
+ var chars []byte
+ if target.awayMessage != "" {
+ chars = append(chars, 'G')
+ } else {
+ chars = append(chars, 'H')
+ }
+
+ if 0 != target.mode&ircUserModeOperator {
+ chars = append(chars, '*')
+ }
+
+ channelName := "*"
+ if ch != nil {
+ channelName = ch.name
+ if modes, present := ch.userModes[target]; present {
+ chars = ircAppendPrefixes(c, modes, chars)
+ }
+ }
+
+ c.sendReply(RPL_WHOREPLY, channelName,
+ target.username, target.hostname, serverName,
+ target.nickname, string(chars), 0 /* hop count */, target.realname)
+}
+
+func ircMatchSendRPLWHOREPLY(c, target *client, mask string) {
+ isRoommate := false
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ _, presentTarget := ch.userModes[target]
+ if presentClient && presentTarget {
+ isRoommate = true
+ break
+ }
+ }
+ if !isRoommate && 0 != target.mode&ircUserModeInvisible {
+ return
+ }
+
+ if !ircFnmatch(mask, target.hostname) &&
+ !ircFnmatch(mask, target.nickname) &&
+ !ircFnmatch(mask, target.realname) &&
+ !ircFnmatch(mask, serverName) {
+ return
+ }
+
+ // Try to find a channel they're on that's visible to us.
+ var userCh *channel
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ _, presentTarget := ch.userModes[target]
+ if presentTarget && (presentClient ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
+ userCh = ch
+ break
+ }
+ }
+ ircSendRPLWHOREPLY(c, userCh, target)
+}
+
+func ircHandleWHO(msg *message, c *client) {
+ onlyOps := len(msg.params) > 1 && msg.params[1] == "o"
+
+ shownMask, usedMask := "*", "*"
+ if len(msg.params) > 0 {
+ shownMask = msg.params[0]
+ if shownMask != "0" {
+ usedMask = shownMask
+ }
+ }
+
+ if ch := channels[ircToCanon(usedMask)]; ch != nil {
+ _, present := ch.userModes[c]
+ if present || 0 == ch.modes&ircChanModeSecret {
+ for client := range ch.userModes {
+ if (present || 0 == client.mode&ircUserModeInvisible) &&
+ (!onlyOps || 0 != client.mode&ircUserModeOperator) {
+ ircSendRPLWHOREPLY(c, ch, client)
+ }
+ }
+ }
+ } else {
+ for _, client := range users {
+ if !onlyOps || 0 != client.mode&ircUserModeOperator {
+ ircMatchSendRPLWHOREPLY(c, client, usedMask)
+ }
+ }
+ }
+ c.sendReply(RPL_ENDOFWHO, shownMask)
+}
+
+func ircSendWHOISReply(c, target *client) {
+ nick := target.nickname
+ c.sendReply(RPL_WHOISUSER, nick,
+ target.username, target.hostname, target.realname)
+ c.sendReply(RPL_WHOISSERVER, nick, serverName, config["server_info"])
+ if 0 != target.mode&ircUserModeOperator {
+ c.sendReply(RPL_WHOISOPERATOR, nick)
+ }
+ c.sendReply(RPL_WHOISIDLE, nick,
+ time.Now().Sub(target.lastActive)/time.Second)
+ if target.awayMessage != "" {
+ c.sendReply(RPL_AWAY, nick, target.awayMessage)
+ }
+
+ var chans []string
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ modes, presentTarget := ch.userModes[target]
+ if presentTarget && (presentClient ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
+ // TODO: Deduplicate, ircAppendPrefixes just also cuts prefixes.
+ var all []byte
+ if 0 != modes&ircChanModeOperator {
+ all = append(all, '@')
+ }
+ if 0 != modes&ircChanModeVoice {
+ all = append(all, '+')
+ }
+ chans = append(chans, string(all)+ch.name)
+ }
+ }
+ c.sendReplyVector(RPL_WHOISCHANNELS, chans, nick, "")
+ c.sendReply(RPL_ENDOFWHOIS, nick)
+}
+
+func ircHandleWHOIS(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ masksStr := msg.params[0]
+ if len(msg.params) > 1 {
+ masksStr = msg.params[1]
+ }
+
+ for _, mask := range splitString(masksStr, ",", true /* ignoreEmpty */) {
+ if strings.IndexAny(mask, "*?") < 0 {
+ if target := users[ircToCanon(mask)]; target == nil {
+ c.sendReply(ERR_NOSUCHNICK, mask)
+ } else {
+ ircSendWHOISReply(c, target)
+ }
+ } else {
+ found := false
+ for _, target := range users {
+ if ircFnmatch(mask, target.nickname) {
+ ircSendWHOISReply(c, target)
+ found = true
+ }
+ }
+ if !found {
+ c.sendReply(ERR_NOSUCHNICK, mask)
+ }
+ }
+ }
+}
+
+func ircHandleWHOWAS(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if len(msg.params) > 2 && !ircIsThisMe(msg.params[2]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[2])
+ return
+ }
+ // The "count" parameter is ignored, we only store one entry for a nick.
+
+ for _, nick := range splitString(msg.params[0], ",", true) {
+ if info := whowas[ircToCanon(nick)]; info == nil {
+ c.sendReply(ERR_WASNOSUCHNICK, nick)
+ } else {
+ c.sendReply(RPL_WHOWASUSER, nick,
+ info.username, info.hostname, info.realname)
+ c.sendReply(RPL_WHOISSERVER, nick,
+ serverName, config["server_info"])
+ }
+ c.sendReply(RPL_ENDOFWHOWAS, nick)
+ }
+}
+
+func ircSendRPLTOPIC(c *client, ch *channel) {
+ if ch.topic == "" {
+ c.sendReply(RPL_NOTOPIC, ch.name)
+ } else {
+ c.sendReply(RPL_TOPIC, ch.name, ch.topic)
+ c.sendReply(RPL_TOPICWHOTIME,
+ ch.name, ch.topicWho, ch.topicTime.Unix())
+ }
+}
+
+func ircHandleTOPIC(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ channelName := msg.params[0]
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if len(msg.params) < 2 {
+ ircSendRPLTOPIC(c, ch)
+ return
+ }
+
+ modes, present := ch.userModes[c]
+ if !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+
+ if 0 != ch.modes&ircChanModeProtectedTopic &&
+ 0 == modes&ircChanModeOperator {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ ch.topic = msg.params[1]
+ ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
+ ch.topicTime = time.Now()
+
+ message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
+ c.nickname, c.username, c.hostname, channelName, ch.topic)
+ ircChannelMulticast(ch, message, nil)
+}
+
+func ircTryPart(c *client, channelName string, reason string) {
+ if reason == "" {
+ reason = c.nickname
+ }
+
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if _, present := ch.userModes[c]; !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+
+ message := fmt.Sprintf(":%s@%s@%s PART %s :%s",
+ c.nickname, c.username, c.hostname, channelName, reason)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ delete(ch.userModes, c)
+ ircChannelDestroyIfEmpty(ch)
+}
+
+func ircPartAllChannels(c *client) {
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present {
+ ircTryPart(c, ch.name, "")
+ }
+ }
+}
+
+func ircHandlePART(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ reason := ""
+ if len(msg.params) > 1 {
+ reason = msg.params[1]
+ }
+
+ for _, channelName := range splitString(msg.params[0], ",", true) {
+ ircTryPart(c, channelName, reason)
+ }
+}
+
+func ircTryKick(c *client, channelName, nick, reason string) {
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if modes, present := ch.userModes[c]; !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ } else if 0 == modes&ircChanModeOperator {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ client := users[ircToCanon(nick)]
+ if _, present := ch.userModes[client]; client == nil || !present {
+ c.sendReply(ERR_USERNOTINCHANNEL, nick, channelName)
+ return
+ }
+
+ message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s",
+ c.nickname, c.username, c.hostname, channelName, nick, reason)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ delete(ch.userModes, client)
+ ircChannelDestroyIfEmpty(ch)
+}
+
+func ircHandleKICK(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ reason := c.nickname
+ if len(msg.params) > 2 {
+ reason = msg.params[2]
+ }
+
+ targetChannels := splitString(msg.params[0], ",", true)
+ targetUsers := splitString(msg.params[1], ",", true)
+
+ if len(channels) == 1 {
+ for i := 0; i < len(targetUsers); i++ {
+ ircTryKick(c, targetChannels[0], targetUsers[i], reason)
+ }
+ } else {
+ for i := 0; i < len(channels) && i < len(targetUsers); i++ {
+ ircTryKick(c, targetChannels[i], targetUsers[i], reason)
+ }
+ }
+}
+
+func ircSendInviteNotifications(ch *channel, c, target *client) {
+ for client := range ch.userModes {
+ if client != target && 0 != client.capsEnabled&ircCapInviteNotify {
+ client.sendf(":%s!%s@%s INVITE %s %s",
+ c.nickname, c.username, c.hostname, target.nickname, ch.name)
+ }
+ }
+}
+
+func ircHandleINVITE(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ target, channelName := msg.params[0], msg.params[1]
+ client := users[ircToCanon(target)]
+ if client == nil {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ return
+ }
+
+ if ch := channels[ircToCanon(channelName)]; ch != nil {
+ invitingModes, invitingPresent := ch.userModes[c]
+ if !invitingPresent {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+ if _, present := ch.userModes[client]; present {
+ c.sendReply(ERR_USERONCHANNEL, target, channelName)
+ return
+ }
+
+ if 0 != invitingModes&ircChanModeOperator {
+ client.invites[ircToCanon(channelName)] = true
+ } else if 0 != ch.modes&ircChanModeInviteOnly {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ // It's not specified when and how we should send out invite-notify.
+ if 0 != ch.modes&ircChanModeInviteOnly {
+ ircSendInviteNotifications(ch, c, client)
+ }
+ }
+
+ client.sendf(":%s!%s@%s INVITE %s %s",
+ c.nickname, c.username, c.hostname, client.nickname, channelName)
+ if client.awayMessage != "" {
+ c.sendReply(RPL_AWAY, client.nickname, client.awayMessage)
+ }
+ c.sendReply(RPL_INVITING, client.nickname, channelName)
+}
+
+func ircTryJoin(c *client, channelName, key string) {
+ ch := channels[ircToCanon(channelName)]
+ var userMode uint
+ if ch == nil {
+ if !ircIsValidChannelName(channelName) {
+ c.sendReply(ERR_BADCHANMASK, channelName)
+ return
+ }
+ ch = ircChannelCreate(channelName)
+ userMode = ircChanModeOperator
+ } else if _, present := ch.userModes[c]; present {
+ return
+ }
+
+ _, invitedByChanop := c.invites[ircToCanon(channelName)]
+ if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) &&
+ !invitedByChanop {
+ c.sendReply(ERR_INVITEONLYCHAN, channelName)
+ return
+ }
+ if ch.key != "" && (key == "" || key != ch.key) {
+ c.sendReply(ERR_BADCHANNELKEY, channelName)
+ return
+ }
+ if ch.userLimit != -1 && len(ch.userModes) >= ch.userLimit {
+ c.sendReply(ERR_CHANNELISFULL, channelName)
+ return
+ }
+ if c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList) &&
+ !invitedByChanop {
+ c.sendReply(ERR_BANNEDFROMCHAN, channelName)
+ return
+ }
+
+ // Destroy any invitation as there's no other way to get rid of it.
+ delete(c.invites, ircToCanon(channelName))
+
+ ch.userModes[c] = userMode
+
+ message := fmt.Sprintf(":%s!%s@%s JOIN %s",
+ c.nickname, c.username, c.hostname, channelName)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ ircSendRPLTOPIC(c, ch)
+ ircSendRPLNAMREPLY(c, ch, nil)
+ c.sendReply(RPL_ENDOFNAMES, ch.name)
+}
+
+func ircHandleJOIN(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ if msg.params[0] == "0" {
+ ircPartAllChannels(c)
+ return
+ }
+
+ targetChannels := splitString(msg.params[0], ",", true)
+
+ var keys []string
+ if len(msg.params) > 1 {
+ keys = splitString(msg.params[1], ",", true)
+ }
+
+ for i, name := range targetChannels {
+ key := ""
+ if i < len(keys) {
+ key = keys[i]
+ }
+ ircTryJoin(c, name, key)
+ }
+}
+
+func ircHandleSUMMON(msg *message, c *client) {
+ c.sendReply(ERR_SUMMONDISABLED)
+}
+
+func ircHandleUSERS(msg *message, c *client) {
+ c.sendReply(ERR_USERSDISABLED)
+}
+
+func ircHandleAWAY(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.awayMessage = ""
+ c.sendReply(RPL_UNAWAY)
+ } else {
+ c.awayMessage = msg.params[0]
+ c.sendReply(RPL_NOWAWAY)
+ }
+}
+
+func ircHandleISON(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ var on []string
+ for _, nick := range msg.params {
+ if client := users[ircToCanon(nick)]; client != nil {
+ on = append(on, nick)
+ }
+ }
+ c.sendReply(RPL_ISON, strings.Join(on, " "))
+}
+
+func ircHandleADMIN(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+ c.sendReply(ERR_NOADMININFO, serverName)
+}
+
+func ircHandleStatsLinks(c *client, msg *message) {
+ // There is only an "l" query in RFC 2812 but we cannot link,
+ // so instead we provide the "L" query giving information for all users.
+ filter := ""
+ if len(msg.params) > 1 {
+ filter = msg.params[1]
+ }
+
+ for _, client := range users {
+ if filter != "" && !ircEqual(client.nickname, filter) {
+ continue
+ }
+ c.sendReply(RPL_STATSLINKINFO,
+ client.address, // linkname
+ len(client.sendQ), // sendq
+ client.nSentMessages, client.sentBytes/1024,
+ client.nReceivedMessages, client.receivedBytes/1024,
+ time.Now().Sub(client.opened)/time.Second)
+ }
+}
+
+func ircHandleStatsCommands(c *client) {
+ for name, handler := range ircHandlers {
+ if handler.nReceived > 0 {
+ c.sendReply(RPL_STATSCOMMANDS, name,
+ handler.nReceived, handler.bytesReceived, 0)
+ }
+ }
+}
+
+// We need to do it this way because of an initialization loop concerning
+// ircHandlers. Workaround proposed by rsc in go #1817.
+var ircHandleStatsCommandsIndirect func(c *client)
+
+func init() {
+ ircHandleStatsCommandsIndirect = ircHandleStatsCommands
+}
+
+func ircHandleStatsUptime(c *client) {
+ uptime := time.Now().Sub(started) / time.Second
+
+ days := uptime / 60 / 60 / 24
+ hours := (uptime % (60 * 60 * 24)) / 60 / 60
+ mins := (uptime % (60 * 60)) / 60
+ secs := uptime % 60
+
+ c.sendReply(RPL_STATSUPTIME, days, hours, mins, secs)
+}
+
+func ircHandleSTATS(msg *message, c *client) {
+ var query byte
+ if len(msg.params) > 0 && len(msg.params[0]) > 0 {
+ query = msg.params[0][0]
+ }
+
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ return
+ }
+
+ switch query {
+ case 'L':
+ ircHandleStatsLinks(c, msg)
+ case 'm':
+ ircHandleStatsCommandsIndirect(c)
+ case 'u':
+ ircHandleStatsUptime(c)
+ }
+ c.sendReply(RPL_ENDOFSTATS, query)
+}
+
+func ircHandleLINKS(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ mask := "*"
+ if len(msg.params) > 0 {
+ if len(msg.params) > 1 {
+ mask = msg.params[1]
+ } else {
+ mask = msg.params[0]
+ }
+ }
+
+ if ircFnmatch(mask, serverName) {
+ c.sendReply(RPL_LINKS, mask, serverName,
+ 0 /* hop count */, config["server_info"])
+ }
+ c.sendReply(RPL_ENDOFLINKS, mask)
+}
+
+func ircHandleKILL(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ return
+ }
+
+ target := users[ircToCanon(msg.params[0])]
+ if target == nil {
+ c.sendReply(ERR_NOSUCHNICK, msg.params[0])
+ return
+ }
+
+ c.sendf(":%s!%s@%s KILL %s :%s",
+ c.nickname, c.username, c.hostname, target.nickname, msg.params[1])
+ target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1]))
+}
+
+func ircHandleDIE(msg *message, c *client) {
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ } else if !quitting {
+ initiateQuit()
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+// TODO: Add an index for ERR_NOSUCHSERVER validation?
+// TODO: Add a minimal parameter count?
+// TODO: Add a field for oper-only commands? Use flags?
+var ircHandlers = map[string]*ircCommand{
+ "CAP": {false, ircHandleCAP, 0, 0},
+ "PASS": {false, ircHandlePASS, 0, 0},
+ "NICK": {false, ircHandleNICK, 0, 0},
+ "USER": {false, ircHandleUSER, 0, 0},
+
+ "USERHOST": {true, ircHandleUSERHOST, 0, 0},
+ "LUSERS": {true, ircHandleLUSERS, 0, 0},
+ "MOTD": {true, ircHandleMOTD, 0, 0},
+ "PING": {true, ircHandlePING, 0, 0},
+ "PONG": {false, ircHandlePONG, 0, 0},
+ "QUIT": {false, ircHandleQUIT, 0, 0},
+ "TIME": {true, ircHandleTIME, 0, 0},
+ "VERSION": {true, ircHandleVERSION, 0, 0},
+ "USERS": {true, ircHandleUSERS, 0, 0},
+ "SUMMON": {true, ircHandleSUMMON, 0, 0},
+ "AWAY": {true, ircHandleAWAY, 0, 0},
+ "ADMIN": {true, ircHandleADMIN, 0, 0},
+ "STATS": {true, ircHandleSTATS, 0, 0},
+ "LINKS": {true, ircHandleLINKS, 0, 0},
+
+ "MODE": {true, ircHandleMODE, 0, 0},
+ "PRIVMSG": {true, ircHandlePRIVMSG, 0, 0},
+ "NOTICE": {true, ircHandleNOTICE, 0, 0},
+ "JOIN": {true, ircHandleJOIN, 0, 0},
+ "PART": {true, ircHandlePART, 0, 0},
+ "KICK": {true, ircHandleKICK, 0, 0},
+ "INVITE": {true, ircHandleINVITE, 0, 0},
+ "TOPIC": {true, ircHandleTOPIC, 0, 0},
+ "LIST": {true, ircHandleLIST, 0, 0},
+ "NAMES": {true, ircHandleNAMES, 0, 0},
+ "WHO": {true, ircHandleWHO, 0, 0},
+ "WHOIS": {true, ircHandleWHOIS, 0, 0},
+ "WHOWAS": {true, ircHandleWHOWAS, 0, 0},
+ "ISON": {true, ircHandleISON, 0, 0},
+
+ "KILL": {true, ircHandleKILL, 0, 0},
+ "DIE": {true, ircHandleDIE, 0, 0},
+}
+
+func ircProcessMessage(c *client, msg *message, raw string) {
+ if c.closing {
+ return
+ }
+
+ c.nReceivedMessages++
+ c.receivedBytes += len(raw) + 2
+
+ if !c.antiflood.check() {
+ c.closeLink("Excess flood")
+ return
+ }
+
+ if cmd, ok := ircHandlers[ircToCanon(msg.command)]; !ok {
+ c.sendReply(ERR_UNKNOWNCOMMAND, msg.command)
+ } else {
+ cmd.nReceived++
+ cmd.bytesReceived += len(raw) + 2
+
+ if cmd.requiresRegistration && !c.registered {
+ c.sendReply(ERR_NOTREGISTERED)
+ } else {
+ cmd.handler(msg, c)
+ }
+ }
+}
+
+// --- Network I/O -------------------------------------------------------------
+
+// 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)
+ } else if tlsConf != nil {
+ c.tls = tls.Server(c.transport, tlsConf)
+ c.conn = c.tls
+ } else {
+ c.printDebug("could not initialize TLS: disabled")
+ c.kill("TLS support disabled")
+ return
+ }
+
+ c.hostname = host
+ c.address = net.JoinHostPort(host, c.port)
+
+ // If we tried to send any data before now, we would need to flushSendQ.
+ go read(c)
+ c.reading = true
+ c.setPingTimer()
+}
+
+// Handle the results from trying to read from the client connection.
+func (c *client) onRead(data []byte, readErr error) {
+ if !c.reading {
+ // Abusing the flag to emulate CloseRead and skip over data, see below.
+ return
+ }
+
+ c.recvQ = append(c.recvQ, data...)
+ for {
+ // XXX: This accepts even simple LF newlines, even though they're not
+ // really allowed by the protocol.
+ advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
+ if advance == 0 {
+ break
+ }
+
+ // XXX: And since it accepts LF, we miscalculate receivedBytes within.
+ c.recvQ = c.recvQ[advance:]
+ line := string(token)
+ c.printDebug("-> %s", line)
+
+ if msg := ircParseMessage(line); msg == nil {
+ c.printDebug("error: invalid line")
+ } else {
+ ircProcessMessage(c, msg, line)
+ }
+ }
+
+ if readErr != nil {
+ c.reading = false
+
+ if readErr != io.EOF {
+ c.printDebug("%s", 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("")
+ } else {
+ c.printDebug("client EOF")
+ c.closeLink("")
+ }
+ } else if len(c.recvQ) > 8192 {
+ c.closeLink("recvQ overrun")
+
+ // tls.Conn doesn't have the CloseRead method (and it needs to be able
+ // to read from the TCP connection even for writes, so there isn't much
+ // sense in expecting the implementation to do anything useful),
+ // otherwise we'd use it to block incoming packet data.
+ c.reading = false
+ }
+}
+
+// Spawn a goroutine to flush the sendQ if possible and necessary.
+func (c *client) flushSendQ() {
+ if !c.writing && c.conn != nil {
+ go write(c, c.sendQ)
+ c.writing = true
+ }
+}
+
+// Handle the results from trying to write to the client connection.
+func (c *client) onWrite(written int, writeErr error) {
+ c.sendQ = c.sendQ[written:]
+ c.writing = false
+
+ if writeErr != nil {
+ c.printDebug("%s", writeErr)
+ c.kill(writeErr.Error())
+ } else if len(c.sendQ) > 0 {
+ c.flushSendQ()
+ } else if c.closing {
+ if c.reading {
+ c.conn.CloseWrite()
+ } else {
+ c.kill("")
+ }
+ }
+}
+
+// --- Worker goroutines -------------------------------------------------------
+
+func accept(ln net.Listener) {
+ for {
+ // Error handling here may be tricky, see go #6163, #24808.
+ if conn, err := ln.Accept(); err != nil {
+ // See go #4373, they're being dicks. Another solution would be to
+ // pass a done channel to this function and close it before closing
+ // all the listeners, returning from here if it's readable.
+ if strings.Contains(err.Error(),
+ "use of closed network connection") {
+ return
+ }
+ if op, ok := err.(net.Error); !ok || !op.Temporary() {
+ exitFatal("%s", err)
+ } else {
+ printError("%s", err)
+ }
+ } else {
+ // TCP_NODELAY is set by default on TCPConns.
+ conns <- conn
+ }
+ }
+}
+
+func prepare(client *client) {
+ conn, host := client.transport, client.hostname
+
+ // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
+ // bothering with pointless contexts.
+ ch := make(chan string, 1)
+ go func() {
+ defer close(ch)
+ if names, err := net.LookupAddr(host); err != nil {
+ printError("%s", err)
+ } else {
+ ch <- names[0]
+ }
+ }()
+
+ // While we can't cancel it, we still want to set a timeout on it.
+ select {
+ case <-time.After(5 * time.Second):
+ case resolved, ok := <-ch:
+ if ok {
+ host = resolved
+ }
+ }
+
+ // Note that in this demo application the autodetection prevents non-TLS
+ // clients from receiving any messages until they send something.
+ 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)
+ } else {
+ isTLS = detectTLS(sysconn)
+ }
+
+ prepared <- preparedEvent{client, host, isTLS}
+}
+
+func read(client *client) {
+ // A new buffer is allocated each time we receive some bytes, because of
+ // thread-safety. Therefore the buffer shouldn't be too large, or we'd
+ // need to copy it each time into a precisely sized new buffer.
+ var err error
+ for err == nil {
+ var (
+ buf [512]byte
+ n int
+ )
+ n, err = client.conn.Read(buf[:])
+ reads <- readEvent{client, buf[:n], err}
+ }
+}
+
+// Flush sendQ, which is passed by parameter so that there are no data races.
+func write(client *client, data []byte) {
+ // We just write as much as we can, the main goroutine does the looping.
+ n, err := client.conn.Write(data)
+ writes <- writeEvent{client, n, err}
+}
+
+// --- Event loop --------------------------------------------------------------
+
+func processOneEvent() {
+ select {
+ case <-sigs:
+ if quitting {
+ forceQuit("requested by user")
+ } else {
+ initiateQuit()
+ }
+
+ case <-quitTimer:
+ forceQuit("timeout")
+
+ case callback := <-timers:
+ callback()
+
+ case conn := <-conns:
+ if maxConnections > 0 && len(clients) >= maxConnections {
+ printDebug("connection limit reached, refusing connection")
+ conn.Close()
+ break
+ }
+
+ 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)
+ }
+
+ c := &client{
+ transport: conn,
+ address: address,
+ hostname: host,
+ port: port,
+ capVersion: 301,
+ opened: time.Now(),
+ lastActive: time.Now(),
+ // 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:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onPrepared(ev.host, ev.isTLS)
+ }
+
+ case ev := <-reads:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onRead(ev.data, ev.err)
+ }
+
+ case ev := <-writes:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onWrite(ev.written, ev.err)
+ }
+ }
+}
+
+// --- Application setup -------------------------------------------------------
+
+func ircInitializeTLS() error {
+ configCert, configKey := config["tls_cert"], config["tls_key"]
+
+ // Only try to enable SSL support if the user configures it; it is not
+ // a failure if no one has requested it.
+ if configCert == "" && configKey == "" {
+ return nil
+ } else if configCert == "" {
+ return errors.New("no TLS certificate set")
+ } else if configKey == "" {
+ return errors.New("no TLS private key set")
+ }
+
+ pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
+ if pathCert == "" {
+ return fmt.Errorf("cannot find file: %s", configCert)
+ }
+
+ pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
+ if pathKey == "" {
+ return fmt.Errorf("cannot find file: %s", configKey)
+ }
+
+ cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
+ if err != nil {
+ return err
+ }
+
+ tlsConf = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ClientAuth: tls.RequestClientCert,
+ SessionTicketsDisabled: true,
+ }
+ return nil
+}
+
+func ircInitializeCatalog() error {
+ configCatalog := config["catalog"]
+ if configCatalog == "" {
+ return nil
+ }
+
+ path := resolveFilename(configCatalog, resolveRelativeConfigFilename)
+ if path == "" {
+ return fmt.Errorf("cannot find file: %s", configCatalog)
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed reading the MOTD file: %s", err)
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ catalog = make(map[int]string)
+ for lineNo := 1; scanner.Scan(); lineNo++ {
+ line := strings.TrimLeft(scanner.Text(), " \t")
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ delim := strings.IndexAny(line, " \t")
+ if delim < 0 {
+ return fmt.Errorf("%s:%d: malformed line", path, lineNo)
+ }
+
+ id, err := strconv.ParseUint(line[:delim], 10, 16)
+ if err != nil {
+ return fmt.Errorf("%s:%d: %s", path, lineNo, err)
+ }
+
+ catalog[int(id)] = line[delim+1:]
+ }
+ return scanner.Err()
+}
+
+func ircInitializeMOTD() error {
+ configMOTD := config["motd"]
+ if configMOTD == "" {
+ return nil
+ }
+
+ path := resolveFilename(configMOTD, resolveRelativeConfigFilename)
+ if path == "" {
+ return fmt.Errorf("cannot find file: %s", configMOTD)
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed reading the MOTD file: %s", err)
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ motd = nil
+ for scanner.Scan() {
+ motd = append(motd, scanner.Text())
+ }
+ return scanner.Err()
+}
+
+type configProcessor struct {
+ err error // any error that has occurred so far
+}
+
+func (cp *configProcessor) read(name string, process func(string) string) {
+ if cp.err != nil {
+ return
+ }
+ if err := process(config[name]); err != "" {
+ cp.err = fmt.Errorf("invalid configuration value for `%s': %s",
+ name, err)
+ }
+}
+
+// This function handles values that require validation before their first use,
+// or some kind of a transformation (such as conversion to an integer) needs
+// to be done before they can be used directly.
+func ircParseConfig() error {
+ cp := &configProcessor{}
+ cp.read("ping_interval", func(value string) string {
+ if u, err := strconv.ParseUint(
+ config["ping_interval"], 10, 32); err != nil {
+ return err.Error()
+ } else if u < 1 {
+ return "the value is out of range"
+ } else {
+ pingInterval = time.Second * time.Duration(u)
+ }
+ return ""
+ })
+ cp.read("max_connections", func(value string) string {
+ if i, err := strconv.ParseInt(
+ value, 10, 32); err != nil {
+ return err.Error()
+ } else if i < 0 {
+ return "the value is out of range"
+ } else {
+ maxConnections = int(i)
+ }
+ return ""
+ })
+ cp.read("operators", func(value string) string {
+ operators = make(map[string]bool)
+ for _, fp := range splitString(value, ",", true) {
+ if !ircIsValidFingerprint(fp) {
+ return "invalid fingerprint value"
+ }
+ operators[strings.ToLower(fp)] = true
+ }
+ return ""
+ })
+ return cp.err
+}
+
+func ircInitializeServerName() error {
+ if value := config["server_name"]; value != "" {
+ if err := ircValidateHostname(value); err != nil {
+ return err
+ }
+ serverName = value
+ return nil
+ }
+
+ if hostname, err := os.Hostname(); err != nil {
+ return err
+ } else if err := ircValidateHostname(hostname); err != nil {
+ return err
+ } else {
+ serverName = hostname
+ }
+ return nil
+}
+
+func ircSetupListenFDs() error {
+ for _, address := range splitString(config["bind"], ",", true) {
+ ln, err := net.Listen("tcp", address)
+ if err != nil {
+ 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")
+ }
+ for _, ln := range listeners {
+ go accept(ln)
+ }
+ return nil
+}
+
+// --- Main --------------------------------------------------------------------
+
+func main() {
+ flag.BoolVar(&debugMode, "debug", false, "run in verbose 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()
+
+ if *version {
+ fmt.Printf("%s %s\n", projectName, projectVersion)
+ return
+ }
+ if *writeDefaultCfg {
+ callSimpleConfigWriteDefault("", configTable)
+ return
+ }
+ if *systemd {
+ logMessage = logMessageSystemd
+ }
+ if flag.NArg() > 0 {
+ flag.Usage()
+ os.Exit(2)
+ }
+
+ config = make(simpleConfig)
+ config.loadDefaults(configTable)
+ if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
+ printError("error loading configuration: %s", err)
+ os.Exit(1)
+ }
+
+ started = time.Now()
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for _, fn := range []func() error{
+ ircInitializeTLS,
+ ircInitializeServerName,
+ ircInitializeMOTD,
+ ircInitializeCatalog,
+ ircParseConfig,
+ ircSetupListenFDs,
+ } {
+ if err := fn(); err != nil {
+ exitFatal("%s", err)
+ }
+ }
+
+ for !quitting || len(clients) > 0 {
+ processOneEvent()
+ }
+}
diff --git a/hid/main_test.go b/hid/main_test.go
new file mode 100644
index 0000000..4d6ccb3
--- /dev/null
+++ b/hid/main_test.go
@@ -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.
+}
diff --git a/hnc/main.go b/hnc/main.go
new file mode 100644
index 0000000..bc10168
--- /dev/null
+++ b/hnc/main.go
@@ -0,0 +1,150 @@
+//
+// Copyright (c) 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.
+//
+
+// hnc is a netcat-alike that shuts down properly.
+package main
+
+import (
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "os"
+)
+
+// #include <unistd.h>
+import "C"
+
+func isatty(fd uintptr) bool { return C.isatty(C.int(fd)) != 0 }
+
+func log(format string, args ...interface{}) {
+ msg := fmt.Sprintf(format+"\n", args...)
+ if isatty(os.Stderr.Fd()) {
+ msg = "\x1b[0;1;31m" + msg + "\x1b[m"
+ }
+ os.Stderr.WriteString(msg)
+}
+
+var (
+ flagTLS = flag.Bool("tls", false, "connect using TLS")
+ flagCRLF = flag.Bool("crlf", false, "translate LF into CRLF")
+)
+
+// Network connection that can shut down the write end.
+type connCloseWriter interface {
+ net.Conn
+ CloseWrite() error
+}
+
+func dial(address string) (connCloseWriter, error) {
+ if *flagTLS {
+ return tls.Dial("tcp", address, &tls.Config{
+ InsecureSkipVerify: true,
+ })
+ }
+ transport, err := net.Dial("tcp", address)
+ if err != nil {
+ return nil, err
+ }
+ return transport.(connCloseWriter), nil
+}
+
+func expand(raw []byte) []byte {
+ if !*flagCRLF {
+ return raw
+ }
+ var res []byte
+ for _, b := range raw {
+ if b == '\n' {
+ res = append(res, '\r')
+ }
+ res = append(res, b)
+ }
+ return res
+}
+
+// Asynchronously delivered result of io.Reader.
+type readResult struct {
+ b []byte
+ err error
+}
+
+func read(r io.Reader, ch chan<- readResult) {
+ defer close(ch)
+ for {
+ var buf [8192]byte
+ n, err := r.Read(buf[:])
+ ch <- readResult{buf[:n], err}
+ if err != nil {
+ break
+ }
+ }
+}
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(),
+ "Usage: %s [OPTION]... HOST PORT\n"+
+ "Connect to a remote host over TCP/IP.\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+
+ flag.Parse()
+ if flag.NArg() != 2 {
+ flag.Usage()
+ os.Exit(2)
+ }
+
+ conn, err := dial(net.JoinHostPort(flag.Arg(0), flag.Arg(1)))
+ if err != nil {
+ log("dial: %s", err)
+ os.Exit(1)
+ }
+
+ fromUser := make(chan readResult)
+ go read(os.Stdin, fromUser)
+
+ fromConn := make(chan readResult)
+ go read(conn, fromConn)
+
+ for fromUser != nil || fromConn != nil {
+ select {
+ case result := <-fromUser:
+ if len(result.b) > 0 {
+ if _, err := conn.Write(expand(result.b)); err != nil {
+ log("remote: %s", err)
+ }
+ }
+ if result.err != nil {
+ log("stdin: %s", result.err)
+ fromUser = nil
+ if err := conn.CloseWrite(); err != nil {
+ log("remote: %s", err)
+ }
+ }
+ case result := <-fromConn:
+ if len(result.b) > 0 {
+ if _, err := os.Stdout.Write(result.b); err != nil {
+ log("stdout: %s", err)
+ }
+ }
+ if result.err != nil {
+ log("remote: %s", result.err)
+ fromConn = nil
+ }
+ }
+ }
+}
diff --git a/prototypes/tls-autodetect.go b/prototypes/tls-autodetect.go
new file mode 100644
index 0000000..0427465
--- /dev/null
+++ b/prototypes/tls-autodetect.go
@@ -0,0 +1,451 @@
+//
+// Copyright (c) 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.
+//
+
+//
+// This is an example TLS-autodetecting chat server.
+//
+// These clients are unable to properly shutdown the connection on their exit:
+// telnet localhost 1234
+// openssl s_client -connect localhost:1234
+//
+// While this one doesn't react to an EOF from the server:
+// ncat -C localhost 1234
+// ncat -C --ssl localhost 1234
+//
+package main
+
+import (
+ "bufio"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+// --- Utilities ---------------------------------------------------------------
+
+//
+// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
+// must be at least three octets long for this to work reliably, but that should
+// not pose a problem in practice. We might try waiting for them.
+//
+// SSL2: 1xxx xxxx | xxxx xxxx | <1>
+// (message length) (client hello)
+// SSL3/TLS: <22> | <3> | xxxx xxxx
+// (handshake)| (protocol version)
+//
+func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
+ sysconn.Read(func(fd uintptr) (done bool) {
+ var buf [3]byte
+ n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
+ switch {
+ case n == 3:
+ isTLS = buf[0]&0x80 != 0 && buf[2] == 1
+ fallthrough
+ case n == 2:
+ isTLS = isTLS || buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
+ }
+ return true
+ })
+ return isTLS
+}
+
+// --- Declarations ------------------------------------------------------------
+
+type connCloseWriter interface {
+ net.Conn
+ CloseWrite() error
+}
+
+type client struct {
+ transport net.Conn // underlying connection
+ tls *tls.Conn // TLS, if detected
+ conn connCloseWriter // high-level connection
+ inQ []byte // unprocessed input
+ outQ []byte // unprocessed output
+ reading bool // whether a reading goroutine is running
+ writing bool // whether a writing goroutine is running
+ closing bool // whether we're closing the connection
+ killTimer *time.Timer // timeout
+}
+
+type preparedEvent struct {
+ client *client
+ host string // client's hostname or literal IP address
+ isTLS bool // the client seems to use TLS
+}
+
+type readEvent struct {
+ client *client
+ data []byte // new data from the client
+ err error // read error
+}
+
+type writeEvent struct {
+ client *client
+ written int // amount of bytes written
+ err error // write error
+}
+
+var (
+ sigs = make(chan os.Signal, 1)
+ conns = make(chan net.Conn)
+ prepared = make(chan preparedEvent)
+ reads = make(chan readEvent)
+ writes = make(chan writeEvent)
+ timeouts = make(chan *client)
+
+ tlsConf *tls.Config
+ clients = make(map[*client]bool)
+ listener net.Listener
+ inShutdown bool
+ shutdownTimer <-chan time.Time
+)
+
+// --- Server ------------------------------------------------------------------
+
+// Broadcast to all /other/ clients (telnet-friendly, also in accordance to
+// the plan of extending this to an IRCd).
+func broadcast(line string, except *client) {
+ for c := range clients {
+ if c != except {
+ c.send(line)
+ }
+ }
+}
+
+// Initiate a clean shutdown of the whole daemon.
+func initiateShutdown() {
+ log.Println("shutting down")
+ if err := listener.Close(); err != nil {
+ log.Println(err)
+ }
+ for c := range clients {
+ c.closeLink()
+ }
+
+ shutdownTimer = time.After(3 * time.Second)
+ inShutdown = true
+}
+
+// Forcefully tear down all connections.
+func forceShutdown(reason string) {
+ if !inShutdown {
+ log.Fatalln("forceShutdown called without initiateShutdown")
+ }
+
+ log.Printf("forced shutdown (%s)\n", reason)
+ for c := range clients {
+ c.destroy()
+ }
+}
+
+// --- Client ------------------------------------------------------------------
+
+func (c *client) send(line string) {
+ if c.conn != nil && !c.closing {
+ c.outQ = append(c.outQ, (line + "\r\n")...)
+ c.flushOutQ()
+ }
+}
+
+// Tear down the client connection, trying to do so in a graceful manner.
+func (c *client) closeLink() {
+ if c.closing {
+ return
+ }
+ if c.conn == nil {
+ c.destroy()
+ return
+ }
+
+ // Since we send this goodbye, we don't need to call CloseWrite here.
+ c.send("Goodbye")
+ c.killTimer = time.AfterFunc(3*time.Second, func() {
+ timeouts <- c
+ })
+
+ c.closing = true
+}
+
+// Close the connection and forget about the client.
+func (c *client) destroy() {
+ // Try to send a "close notify" alert if the TLS object is ready,
+ // otherwise just tear down the transport.
+ if c.conn != nil {
+ _ = c.conn.Close()
+ } else {
+ _ = c.transport.Close()
+ }
+
+ // Clean up the goroutine, although a spurious event may still be sent.
+ if c.killTimer != nil {
+ c.killTimer.Stop()
+ }
+
+ log.Println("client destroyed")
+ delete(clients, c)
+}
+
+// Handle the results from initializing the client's connection.
+func (c *client) onPrepared(isTLS bool) {
+ if isTLS {
+ c.tls = tls.Server(c.transport, tlsConf)
+ c.conn = c.tls
+ } else {
+ c.conn = c.transport.(connCloseWriter)
+ }
+
+ // TODO: If we've tried to send any data before now, we need to flushOutQ.
+ go read(c)
+ c.reading = true
+}
+
+// Handle the results from trying to read from the client connection.
+func (c *client) onRead(data []byte, readErr error) {
+ if !c.reading {
+ // Abusing the flag to emulate CloseRead and skip over data, see below.
+ return
+ }
+
+ c.inQ = append(c.inQ, data...)
+ for {
+ advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */)
+ if advance == 0 {
+ break
+ }
+
+ c.inQ = c.inQ[advance:]
+ line := string(token)
+ fmt.Println(line)
+ broadcast(line, c)
+ }
+
+ if readErr != nil {
+ c.reading = false
+
+ if readErr != io.EOF {
+ log.Println(readErr)
+ c.destroy()
+ } else if c.closing {
+ // Disregarding whether a clean shutdown has happened or not.
+ log.Println("client finished shutdown")
+ c.destroy()
+ } else {
+ log.Println("client EOF")
+ c.closeLink()
+ }
+ } else if len(c.inQ) > 8192 {
+ log.Println("client inQ overrun")
+ // TODO: Inform the client about inQ overrun in the farewell message.
+ c.closeLink()
+
+ // 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
+ // sense in expecting the implementation to do anything useful),
+ // otherwise we'd use it to block incoming packet data.
+ c.reading = false
+ }
+}
+
+// Spawn a goroutine to flush the outQ if possible and necessary.
+func (c *client) flushOutQ() {
+ if !c.writing && c.conn != nil {
+ go write(c, c.outQ)
+ c.writing = true
+ }
+}
+
+// Handle the results from trying to write to the client connection.
+func (c *client) onWrite(written int, writeErr error) {
+ c.outQ = c.outQ[written:]
+ c.writing = false
+
+ if writeErr != nil {
+ log.Println(writeErr)
+ c.destroy()
+ } else if len(c.outQ) > 0 {
+ c.flushOutQ()
+ } else if c.closing {
+ if c.reading {
+ c.conn.CloseWrite()
+ } else {
+ c.destroy()
+ }
+ }
+}
+
+// --- Worker goroutines -------------------------------------------------------
+
+func accept(ln net.Listener) {
+ for {
+ if conn, err := ln.Accept(); err != nil {
+ // TODO: Consider specific cases in error handling, some errors
+ // are transitional while others are fatal.
+ log.Println(err)
+ break
+ } else {
+ conns <- conn
+ }
+ }
+}
+
+func prepare(client *client) {
+ 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.
+ ch := make(chan string, 1)
+ go func() {
+ defer close(ch)
+ if names, err := net.LookupAddr(host); err != nil {
+ log.Println(err)
+ } else {
+ ch <- names[0]
+ }
+ }()
+
+ // While we can't cancel it, we still want to set a timeout on it.
+ select {
+ case <-time.After(5 * time.Second):
+ case resolved, ok := <-ch:
+ if ok {
+ host = resolved
+ }
+ }
+
+ // Note that in this demo application the autodetection prevents non-TLS
+ // clients from receiving any messages until they send something.
+ 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.
+ log.Println(err)
+ } else {
+ isTLS = detectTLS(sysconn)
+ }
+
+ // FIXME: When the client sends no data, we still initialize its conn.
+ prepared <- preparedEvent{client, host, isTLS}
+}
+
+func read(client *client) {
+ // A new buffer is allocated each time we receive some bytes, because of
+ // thread-safety. Therefore the buffer shouldn't be too large, or we'd
+ // need to copy it each time into a precisely sized new buffer.
+ var err error
+ for err == nil {
+ var (
+ buf [512]byte
+ n int
+ )
+ n, err = client.conn.Read(buf[:])
+ reads <- readEvent{client, buf[:n], err}
+ }
+}
+
+// Flush outQ, which is passed by parameter so that there are no data races.
+func write(client *client, data []byte) {
+ // We just write as much as we can, the main goroutine does the looping.
+ n, err := client.conn.Write(data)
+ writes <- writeEvent{client, n, err}
+}
+
+// --- Main --------------------------------------------------------------------
+
+func processOneEvent() {
+ select {
+ case <-sigs:
+ if inShutdown {
+ forceShutdown("requested by user")
+ } else {
+ initiateShutdown()
+ }
+
+ case <-shutdownTimer:
+ forceShutdown("timeout")
+
+ case conn := <-conns:
+ log.Println("accepted client connection")
+ c := &client{transport: conn}
+ clients[c] = true
+ go prepare(c)
+
+ case ev := <-prepared:
+ log.Println("client is ready, resolved to", ev.host)
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onPrepared(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)
+ }
+
+ case c := <-timeouts:
+ if _, ok := clients[c]; ok {
+ log.Println("client timeouted")
+ c.destroy()
+ }
+ }
+}
+
+func main() {
+ // Just deal with unexpected flags, we don't use any ourselves.
+ flag.Parse()
+
+ if len(flag.Args()) != 3 {
+ log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
+ }
+
+ cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ tlsConf = &tls.Config{Certificates: []tls.Certificate{cert}}
+ listener, err = net.Listen("tcp", flag.Arg(2))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ go accept(listener)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for !inShutdown || len(clients) > 0 {
+ processOneEvent()
+ }
+}
diff --git a/prototypes/xgb-draw.go b/prototypes/xgb-draw.go
new file mode 100644
index 0000000..b893f6f
--- /dev/null
+++ b/prototypes/xgb-draw.go
@@ -0,0 +1,329 @@
+// Network-friendly drawing application based on XRender.
+//
+// TODO: Maybe keep the pixmap as large as the window.
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+)
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func findPictureFormat(formats []render.Pictforminfo,
+ depth byte, direct render.Directformat) render.Pictformat {
+ for _, pf := range formats {
+ if pf.Depth == depth && pf.Direct == direct {
+ return pf.Id
+ }
+ }
+ return 0
+}
+
+func createNewPicture(X *xgb.Conn, depth byte, drawable xproto.Drawable,
+ width uint16, height uint16, format render.Pictformat) render.Picture {
+ pixmapid, err := xproto.NewPixmapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreatePixmap(X, depth, pixmapid, drawable, width, height)
+
+ pictid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreatePicture(X, pictid, xproto.Drawable(pixmapid), format,
+ 0, []uint32{})
+ return pictid
+}
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ if depth < 24 {
+ log.Fatalln("need more colors")
+ }
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwEventMask,
+ []uint32{0xffffffff, xproto.EventMaskButtonPress |
+ xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease |
+ xproto.EventMaskStructureNotify | xproto.EventMaskExposure})
+
+ title := []byte("Draw")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Find appropriate picture formats.
+ var pformat, pformatAlpha, pformatRGB render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+ if pformatAlpha = findPictureFormat(pformats.Formats, 8,
+ render.Directformat{
+ AlphaShift: 0,
+ AlphaMask: 0xff,
+ }); pformat == 0 {
+ log.Fatalln("required picture format not found")
+ }
+ if pformatRGB = findPictureFormat(pformats.Formats, 24,
+ render.Directformat{
+ RedShift: 16,
+ RedMask: 0xff,
+ GreenShift: 8,
+ GreenMask: 0xff,
+ BlueShift: 0,
+ BlueMask: 0xff,
+ }); pformatRGB == 0 {
+ log.Fatalln("required picture format not found")
+ }
+
+ // Picture for the window.
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // Brush shape.
+ const brushRadius = 5
+
+ brushid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff}
+ cTrans := render.Color{0xffff, 0xffff, 0xffff, 0}
+ _ = render.CreateRadialGradient(X, brushid,
+ render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
+ render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
+ F64ToFixed(0),
+ F64ToFixed(brushRadius),
+ 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)},
+ []render.Color{cFull, cFull, cTrans})
+
+ // Brush color.
+ colorid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreateSolidFill(X, colorid, render.Color{
+ Red: 0x4444,
+ Green: 0x8888,
+ Blue: 0xffff,
+ Alpha: 0xffff,
+ })
+
+ // Various pixmaps.
+ const (
+ pixWidth = 1000
+ pixHeight = 1000
+ )
+
+ canvasid := createNewPicture(X, 24,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
+ bufferid := createNewPicture(X, 24,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
+ maskid := createNewPicture(X, 8,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatAlpha)
+
+ // Smoothing by way of blur, apparently a misguided idea.
+ /*
+ _ = render.SetPictureFilter(X, maskid,
+ uint16(len("convolution")), "convolution",
+ []render.Fixed{F64ToFixed(3), F64ToFixed(3),
+ F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0),
+ F64ToFixed(0.15), F64ToFixed(0.40), F64ToFixed(0.15),
+ F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0)})
+ */
+
+ // Pixmaps come uninitialized.
+ _ = render.FillRectangles(X,
+ render.PictOpSrc, canvasid, render.Color{
+ Red: 0xffff, Green: 0xffff, Blue: 0xffff, Alpha: 0xffff,
+ }, []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
+
+ // This is the only method we can use to render brush strokes without
+ // alpha accumulation due to stamping. Though this also seems to be
+ // misguided. Keeping it here for educational purposes.
+ //
+ // ConjointOver is defined as: A = Aa * 1 + Ab * max(1-Aa/Ab,0)
+ // which basically resolves to: A = max(Aa, Ab)
+ // which equals "lighten" with one channel only.
+ //
+ // Resources:
+ // - https://www.cairographics.org/operators/
+ // - http://ssp.impulsetrain.com/porterduff.html
+ // - https://keithp.com/~keithp/talks/renderproblems/renderproblems/render-title.html
+ // - https://keithp.com/~keithp/talks/cairo2003.pdf
+ drawPointAt := func(x, y int16) {
+ _ = render.Composite(X, render.PictOpConjointOver,
+ brushid, render.PictureNone, maskid,
+ 0, 0, 0, 0, x-brushRadius, y-brushRadius,
+ brushRadius*2, brushRadius*2)
+
+ _ = render.SetPictureClipRectangles(X, bufferid,
+ x-brushRadius, y-brushRadius, []xproto.Rectangle{
+ {Width: brushRadius * 2, Height: brushRadius * 2}})
+ _ = render.Composite(X, render.PictOpSrc,
+ canvasid, render.PictureNone, bufferid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, bufferid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ // Composited, now blit to window without flicker.
+ _ = render.SetPictureClipRectangles(X, pid,
+ x-brushRadius, y-brushRadius, []xproto.Rectangle{
+ {Width: brushRadius * 2, Height: brushRadius * 2}})
+ _ = render.Composite(X, render.PictOpSrc,
+ bufferid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ }
+
+ // Integer version of Bresenham's line drawing algorithm
+ drawLine := func(x0, y0, x1, y1 int16) {
+ dx, dy := x1-x0, y1-y0
+ if dx < 0 {
+ dx = -dx
+ }
+ if dy < 0 {
+ dy = -dy
+ }
+
+ steep := dx < dy
+ if steep {
+ // Flip the coordinate system on input
+ x0, y0 = y0, x0
+ x1, y1 = y1, x1
+ dx, dy = dy, dx
+ }
+
+ var stepX, stepY int16 = 1, 1
+ if x0 > x1 {
+ stepX = -1
+ }
+ if y0 > y1 {
+ stepY = -1
+ }
+
+ dpr := dy * 2
+ delta := dpr - dx
+ dpru := delta - dx
+
+ for ; dx > 0; dx-- {
+ // Unflip the coordinate system on output
+ if steep {
+ drawPointAt(y0, x0)
+ } else {
+ drawPointAt(x0, y0)
+ }
+
+ x0 += stepX
+ if delta > 0 {
+ y0 += stepY
+ delta += dpru
+ } else {
+ delta += dpr
+ }
+ }
+ }
+
+ var startX, startY int16 = 0, 0
+ drawing := false
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ExposeEvent:
+ _ = render.SetPictureClipRectangles(X, pid, int16(e.X), int16(e.Y),
+ []xproto.Rectangle{{Width: e.Width, Height: e.Height}})
+
+ // Not bothering to deflicker here using the buffer pixmap,
+ // with compositing this event is rare enough.
+ _ = render.Composite(X, render.PictOpSrc,
+ canvasid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ if drawing {
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ }
+
+ case xproto.ButtonPressEvent:
+ if e.Detail == xproto.ButtonIndex1 {
+ render.FillRectangles(X,
+ render.PictOpSrc, maskid, render.Color{},
+ []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
+
+ drawing = true
+ drawPointAt(e.EventX, e.EventY)
+ startX, startY = e.EventX, e.EventY
+ }
+
+ case xproto.MotionNotifyEvent:
+ if drawing {
+ drawLine(startX, startY, e.EventX, e.EventY)
+ startX, startY = e.EventX, e.EventY
+ }
+
+ case xproto.ButtonReleaseEvent:
+ if e.Detail == xproto.ButtonIndex1 {
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, canvasid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ drawing = false
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-image.go b/prototypes/xgb-image.go
new file mode 100644
index 0000000..2da37d6
--- /dev/null
+++ b/prototypes/xgb-image.go
@@ -0,0 +1,313 @@
+package main
+
+import (
+ "encoding/binary"
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/shm"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+ "os"
+ "reflect"
+ "time"
+ "unsafe"
+
+ "image"
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+)
+
+// #include <sys/ipc.h>
+// #include <sys/shm.h>
+import "C"
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func main() {
+ /*
+ pf, err := os.Create("pprof.out")
+ if err != nil {
+ log.Fatal(err)
+ }
+ pprof.StartCPUProfile(pf)
+ defer pprof.StopCPUProfile()
+ */
+
+ // Load a picture from the command line.
+ f, err := os.Open(os.Args[1])
+ if err != nil {
+ log.Fatalln(err)
+ }
+ defer f.Close()
+
+ img, name, err := image.Decode(f)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ log.Println("image type is", name)
+
+ // Miscellaneous X11 initialization.
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ for _, v := range i.Visuals {
+ // TODO: Could/should check other parameters.
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0x80808080, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskExposure,
+ uint32(mid)})
+
+ title := []byte("Image")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ // Wrap the window's surface in a picture.
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // setup.BitmapFormatScanline{Pad,Unit} and setup.BitmapFormatBitOrder
+ // don't interest us here since we're only using Z format pixmaps.
+ for _, pf := range setup.PixmapFormats {
+ if pf.Depth == 32 {
+ if pf.BitsPerPixel != 32 || pf.ScanlinePad != 32 {
+ log.Fatalln("unsuported X server")
+ }
+ }
+ }
+
+ pixid, err := xproto.NewPixmapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreatePixmap(X, 32, pixid, xproto.Drawable(screen.Root),
+ uint16(img.Bounds().Dx()), uint16(img.Bounds().Dy()))
+
+ var bgraFormat render.Pictformat
+ wanted := render.Directformat{
+ RedShift: 16,
+ RedMask: 0xff,
+ GreenShift: 8,
+ GreenMask: 0xff,
+ BlueShift: 0,
+ BlueMask: 0xff,
+ AlphaShift: 24,
+ AlphaMask: 0xff,
+ }
+ for _, pf := range pformats.Formats {
+ if pf.Depth == 32 && pf.Direct == wanted {
+ bgraFormat = pf.Id
+ break
+ }
+ }
+ if bgraFormat == 0 {
+ log.Fatalln("ARGB format not found")
+ }
+
+ // We could also look for the inverse pictformat.
+ var encoding binary.ByteOrder
+ if setup.ImageByteOrder == xproto.ImageOrderMSBFirst {
+ encoding = binary.BigEndian
+ } else {
+ encoding = binary.LittleEndian
+ }
+
+ pixpicid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pixpicid, xproto.Drawable(pixid), bgraFormat,
+ 0, []uint32{})
+
+ // Do we really need this? :/
+ cid, err := xproto.NewGcontextId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreateGC(X, cid, xproto.Drawable(pixid),
+ xproto.GcGraphicsExposures, []uint32{0})
+
+ bounds := img.Bounds()
+ Lstart := time.Now()
+
+ if err := shm.Init(X); err != nil {
+ log.Println("MIT-SHM unavailable")
+
+ // We're being lazy and resolve the 1<<16 limit of requests by sending
+ // a row at a time. The encoding is also done inefficiently.
+ // Also see xgbutil/xgraphics/xsurface.go.
+ row := make([]byte, bounds.Dx()*4)
+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := img.At(x, y).RGBA()
+ encoding.PutUint32(row[x*4:],
+ (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
+ }
+ _ = xproto.PutImage(X, xproto.ImageFormatZPixmap,
+ xproto.Drawable(pixid), cid, uint16(bounds.Dx()), 1,
+ 0, int16(y),
+ 0, 32, row)
+ }
+ } else {
+ rep, err := shm.QueryVersion(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if rep.PixmapFormat != xproto.ImageFormatZPixmap ||
+ !rep.SharedPixmaps {
+ log.Fatalln("MIT-SHM configuration unfit")
+ }
+
+ shmSize := bounds.Dx() * bounds.Dy() * 4
+
+ // As a side note, to clean up unreferenced segments (orphans):
+ // ipcs -m | awk '$6 == "0" { print $2 }' | xargs ipcrm shm
+ shmID := int(C.shmget(C.IPC_PRIVATE,
+ C.size_t(shmSize), C.IPC_CREAT|0777))
+ if shmID == -1 {
+ // TODO: We should handle this case by falling back to PutImage,
+ // if only because the allocation may hit a system limit.
+ log.Fatalln("memory allocation failed")
+ }
+
+ dataRaw := C.shmat(C.int(shmID), nil, 0)
+ defer C.shmdt(dataRaw)
+ defer C.shmctl(C.int(shmID), C.IPC_RMID, nil)
+
+ data := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+ Data: uintptr(dataRaw), Len: shmSize, Cap: shmSize}))
+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+ row := data[y*bounds.Dx()*4:]
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := img.At(x, y).RGBA()
+ encoding.PutUint32(row[x*4:],
+ (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
+ }
+ }
+
+ segid, err := shm.NewSegId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Need to have it attached on the server before we unload the segment.
+ c := shm.AttachChecked(X, segid, uint32(shmID), true /* RO */)
+ if err := c.Check(); err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = shm.PutImage(X, xproto.Drawable(pixid), cid,
+ uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
+ uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
+ 32, xproto.ImageFormatZPixmap,
+ 0 /* SendEvent */, segid, 0 /* Offset */)
+ }
+
+ log.Println("uploading took", time.Now().Sub(Lstart))
+
+ var scale float64 = 1
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h := e.Width, e.Height
+
+ scaleX := float64(bounds.Dx()) / float64(w)
+ scaleY := float64(bounds.Dy()) / float64(h)
+
+ if scaleX < scaleY {
+ scale = scaleY
+ } else {
+ scale = scaleX
+ }
+
+ _ = render.SetPictureTransform(X, pixpicid, render.Transform{
+ F64ToFixed(scale), F64ToFixed(0), F64ToFixed(0),
+ F64ToFixed(0), F64ToFixed(scale), F64ToFixed(0),
+ F64ToFixed(0), F64ToFixed(0), F64ToFixed(1),
+ })
+ _ = render.SetPictureFilter(X, pixpicid, 8, "bilinear", nil)
+
+ case xproto.ExposeEvent:
+ _ = render.Composite(X, render.PictOpSrc,
+ pixpicid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ uint16(float64(img.Bounds().Dx())/scale),
+ uint16(float64(img.Bounds().Dy())/scale))
+ }
+ }
+}
diff --git a/prototypes/xgb-keys.go b/prototypes/xgb-keys.go
new file mode 100644
index 0000000..578ab39
--- /dev/null
+++ b/prototypes/xgb-keys.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ //"github.com/BurntSushi/xgb/xkb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ /*
+ // Use the extension if available, makes better use of state bits.
+ if err := xkb.Init(X); err == nil {
+ if _, err := xkb.UseExtension(X, 1, 0).Reply(); err != nil {
+ log.Fatalln(err)
+ }
+ }
+ */
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ for _, v := range i.Visuals {
+ // TODO: Could/should check other parameters.
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0x80808080, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ /* KeymapNotify */ xproto.EventMaskKeymapState, uint32(mid)})
+
+ title := []byte("Keys")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ mapping, err := xproto.GetKeyboardMapping(X, setup.MinKeycode,
+ byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
+ mm, err := xproto.GetModifierMapping(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // XXX: This seems pointless, the key will just end up switching groups
+ // instead of levels without full XKB handling. Though perhaps it might
+ // at least work as intended when there's only one XKB group.
+ const MODE_SWITCH = 0xff7e
+
+ var modeSwitchMask uint16
+ for mod := 0; mod < 8; mod++ {
+ perMod := int(mm.KeycodesPerModifier)
+ for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
+ if kc == 0 {
+ continue
+ }
+
+ perKc := int(mapping.KeysymsPerKeycode)
+ k := int(kc - setup.MinKeycode)
+ for _, ks := range mapping.Keysyms[k*perKc : (k+1)*perKc] {
+ if ks == MODE_SWITCH {
+ modeSwitchMask |= 1 << uint(mod)
+ }
+ }
+ }
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.KeymapNotifyEvent:
+ // e.Keys is a 32 * 8 large bitmap indicating which keys are
+ // currently pressed down. This is sent "after every EnterNotify
+ // and FocusIn" but it also seems to fire when the keyboard layout
+ // changes. aixterm manual even speaks of that explicitly.
+ //
+ // But since changing the effective group involves no changes to
+ // the compatibility mapping, there's nothing for us to do.
+
+ case xproto.MappingNotifyEvent:
+ // e.FirstKeyCode .. e.Count changes have happened but rereading
+ // everything is the simpler thing to do.
+ mapping, err = xproto.GetKeyboardMapping(X, setup.MinKeycode,
+ byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // TODO: We should also repeat the search for MODE SWITCH.
+
+ case xproto.KeyPressEvent:
+ step := int(mapping.KeysymsPerKeycode)
+ from := int(e.Detail-setup.MinKeycode) * step
+ ks := mapping.Keysyms[from : from+step]
+
+ // Strip trailing NoSymbol entries.
+ for len(ks) > 0 && ks[len(ks)-1] == 0 {
+ ks = ks[:len(ks)-1]
+ }
+
+ // Expand back to at least 4.
+ switch {
+ case len(ks) == 1:
+ ks = append(ks, 0, ks[0], 0)
+ case len(ks) == 2:
+ ks = append(ks, ks[0], ks[1])
+ case len(ks) == 3:
+ ks = append(ks, 0)
+ }
+
+ // Other silly expansion rules, only applied to basic ASCII.
+ if ks[1] == 0 {
+ ks[1] = ks[0]
+ if ks[0] >= 'A' && ks[0] <= 'Z' ||
+ ks[0] >= 'a' && ks[0] <= 'z' {
+ ks[0] = ks[0] | 32
+ ks[1] = ks[0] &^ 32
+ }
+ }
+
+ if ks[3] == 0 {
+ ks[3] = ks[2]
+ if ks[2] >= 'A' && ks[2] <= 'Z' ||
+ ks[2] >= 'a' && ks[2] <= 'z' {
+ ks[2] = ks[2] | 32
+ ks[3] = ks[2] &^ 32
+ }
+ }
+
+ // We only have enough information to switch between two groups.
+ offset := 0
+ if e.State&modeSwitchMask != 0 {
+ offset += 2
+ }
+
+ var result xproto.Keysym
+
+ shift := e.State&xproto.ModMaskShift != 0
+ lock := e.State&xproto.ModMaskLock != 0
+ switch {
+ case !shift && !lock:
+ result = ks[offset+0]
+ case !shift && lock:
+ if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' {
+ result = ks[offset+1]
+ } else {
+ result = ks[offset+0]
+ }
+ case shift && lock:
+ if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' {
+ result = ks[offset+1] &^ 32
+ } else {
+ result = ks[offset+1]
+ }
+ case shift:
+ result = ks[offset+1]
+ }
+
+ if result <= 0xff {
+ log.Printf("%c (Latin-1)\n", rune(result))
+ } else {
+ log.Println(result)
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-monitors.go b/prototypes/xgb-monitors.go
new file mode 100644
index 0000000..5ce5af9
--- /dev/null
+++ b/prototypes/xgb-monitors.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+
+ // Needs a patched local version with xcb-proto 1.12 and this fix:
+ // -size := xgb.Pad((8 + (24 + xgb.Pad((int(NOutput) * 4)))))
+ // +size := xgb.Pad((8 + (24 + xgb.Pad((int(Monitorinfo.NOutput) * 4)))))
+ "github.com/BurntSushi/xgb/randr"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := randr.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ ms, err := randr.GetMonitors(X, screen.Root, true /* GetActive */).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for _, m := range ms.Monitors {
+ reply, err := xproto.GetAtomName(X, m.Name).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ log.Printf("Monitor %s %+v\n", reply.Name, m)
+ }
+}
diff --git a/prototypes/xgb-text-viewer.go b/prototypes/xgb-text-viewer.go
new file mode 100644
index 0000000..e740138
--- /dev/null
+++ b/prototypes/xgb-text-viewer.go
@@ -0,0 +1,538 @@
+// This is an amalgamation of xgb-xrender.go and xgb-keys.go and more of a demo,
+// some comments have been stripped.
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ "github.com/golang/freetype"
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/math/fixed"
+ "image"
+ "image/draw"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+)
+
+func glyphListBytes(buf []byte, runes []rune, size int) int {
+ b := 0
+ for _, r := range runes {
+ switch size {
+ default:
+ buf[b] = byte(r)
+ b += 1
+ case 2:
+ xgb.Put16(buf[b:], uint16(r))
+ b += 2
+ case 4:
+ xgb.Put32(buf[b:], uint32(r))
+ b += 4
+ }
+ }
+ return xgb.Pad(b)
+}
+
+// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32.
+func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int {
+ b := 0
+ buf[b] = len
+ b += 4
+ xgb.Put16(buf[b:], uint16(deltaX))
+ b += 2
+ xgb.Put16(buf[b:], uint16(deltaY))
+ b += 2
+ return xgb.Pad(b)
+}
+
+type xgbCookie interface{ Check() error }
+
+// compositeString makes an appropriate render.CompositeGlyphs request,
+// assuming that glyphs equal Unicode codepoints.
+func compositeString(c *xgb.Conn, op byte, src, dst render.Picture,
+ maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16,
+ destX, destY int16, text string) xgbCookie {
+ runes := []rune(text)
+
+ var highest rune
+ for _, r := range runes {
+ if r > highest {
+ highest = r
+ }
+ }
+
+ size := 1
+ switch {
+ case highest > 1<<16:
+ size = 4
+ case highest > 1<<8:
+ size = 2
+ }
+
+ // They gave up on the XCB protocol API and we need to serialize explicitly.
+
+ // To spare us from caring about the padding, use the largest number lesser
+ // than 255 that is divisible by 4 (for size 2 and 4 the requirements are
+ // less strict but this works in the general case).
+ const maxPerChunk = 252
+
+ buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size)
+ b := 0
+
+ for len(runes) > maxPerChunk {
+ b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0)
+ b += glyphListBytes(buf[b:], runes[:maxPerChunk], size)
+ runes = runes[maxPerChunk:]
+ }
+ if len(runes) > 0 {
+ b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY)
+ b += glyphListBytes(buf[b:], runes, size)
+ }
+
+ switch size {
+ default:
+ return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 2:
+ return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 4:
+ return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ }
+}
+
+type textRenderer struct {
+ f *truetype.Font
+ opts *truetype.Options
+ face font.Face
+
+ bounds fixed.Rectangle26_6 // outer bounds for all the font's glyph
+ buf *image.RGBA // rendering buffer
+
+ X *xgb.Conn
+ gsid render.Glyphset
+ loaded map[rune]bool
+}
+
+func newTextRenderer(X *xgb.Conn, ttf []byte, opts *truetype.Options) (
+ *textRenderer, error) {
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ return nil, err
+ }
+
+ // We use RGBA here just so that lines are padded to 32 bits.
+ // Since there's no subpixel antialiasing and alpha is premultiplied,
+ // it doesn't even mater that RGBA is interpreted as ARGB or BGRA.
+ var rgbFormat render.Pictformat
+ for _, pf := range pformats.Formats {
+ if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
+ rgbFormat = pf.Id
+ break
+ }
+ }
+
+ tr := &textRenderer{opts: opts, X: X, loaded: make(map[rune]bool)}
+ if tr.f, err = freetype.ParseFont(goregular.TTF); err != nil {
+ return nil, err
+ }
+
+ tr.face = truetype.NewFace(tr.f, opts)
+ tr.bounds = tr.f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
+ (64.0 / 72.0)))
+
+ if tr.gsid, err = render.NewGlyphsetId(X); err != nil {
+ return nil, err
+ }
+ if err := render.CreateGlyphSetChecked(X, tr.gsid, rgbFormat).
+ Check(); err != nil {
+ return nil, err
+ }
+
+ tr.buf = image.NewRGBA(image.Rect(
+ +tr.bounds.Min.X.Floor(),
+ -tr.bounds.Min.Y.Floor(),
+ +tr.bounds.Max.X.Ceil(),
+ -tr.bounds.Max.Y.Ceil(),
+ ))
+ return tr, nil
+}
+
+func (tr *textRenderer) addRune(r rune) bool {
+ dr, mask, maskp, advance, ok := tr.face.Glyph(
+ fixed.P(0, 0) /* subpixel destination location */, r)
+ if !ok {
+ return false
+ }
+
+ for i := 0; i < len(tr.buf.Pix); i++ {
+ tr.buf.Pix[i] = 0
+ }
+
+ // Copying, since there are absolutely no guarantees.
+ draw.Draw(tr.buf, dr, mask, maskp, draw.Src)
+
+ _ = render.AddGlyphs(tr.X, tr.gsid, 1, []uint32{uint32(r)},
+ []render.Glyphinfo{{
+ Width: uint16(tr.buf.Rect.Size().X),
+ Height: uint16(tr.buf.Rect.Size().Y),
+ X: int16(-tr.bounds.Min.X.Floor()),
+ Y: int16(+tr.bounds.Max.Y.Ceil()),
+ XOff: int16(advance.Ceil()),
+ YOff: int16(0),
+ }}, []byte(tr.buf.Pix))
+ return true
+}
+
+func (tr *textRenderer) render(src, dst render.Picture,
+ srcX, srcY, destX, destY int16, text string) xgbCookie {
+ // XXX: You're really supposed to handle tabs differently from this.
+ text = strings.Replace(text, "\t", " ", -1)
+
+ for _, r := range text {
+ if !tr.loaded[r] {
+ tr.addRune(r)
+ tr.loaded[r] = true
+ }
+ }
+
+ return compositeString(tr.X, render.PictOpOver, src, dst,
+ 0 /* TODO: mask Pictureformat? */, tr.gsid,
+ srcX, srcY, destX, destY, text)
+}
+
+const (
+ ksEscape = 0xff1b
+ ksUp = 0xff52
+ ksDown = 0xff54
+ ksPageUp = 0xff55
+ ksPageDown = 0xff56
+ ksModeSwitch = 0xff7e
+)
+
+type keyMapper struct {
+ X *xgb.Conn
+ setup *xproto.SetupInfo
+ mapping *xproto.GetKeyboardMappingReply
+
+ modeSwitchMask uint16
+}
+
+func newKeyMapper(X *xgb.Conn) (*keyMapper, error) {
+ m := &keyMapper{X: X, setup: xproto.Setup(X)}
+ if err := m.update(); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func (km *keyMapper) update() error {
+ var err error
+ km.mapping, err = xproto.GetKeyboardMapping(km.X, km.setup.MinKeycode,
+ byte(km.setup.MaxKeycode-km.setup.MinKeycode+1)).Reply()
+ if err != nil {
+ return err
+ }
+
+ km.modeSwitchMask = 0
+
+ // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
+ mm, err := xproto.GetModifierMapping(km.X).Reply()
+ if err != nil {
+ return err
+ }
+
+ perMod := int(mm.KeycodesPerModifier)
+ perKc := int(km.mapping.KeysymsPerKeycode)
+ for mod := 0; mod < 8; mod++ {
+ for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
+ if kc == 0 {
+ continue
+ }
+
+ k := int(kc - km.setup.MinKeycode)
+ for _, ks := range km.mapping.Keysyms[k*perKc : (k+1)*perKc] {
+ if ks == ksModeSwitch {
+ km.modeSwitchMask |= 1 << uint(mod)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (km *keyMapper) decode(e xproto.KeyPressEvent) (result xproto.Keysym) {
+ step := int(km.mapping.KeysymsPerKeycode)
+ from := int(e.Detail-km.setup.MinKeycode) * step
+ ks := km.mapping.Keysyms[from : from+step]
+
+ // Strip trailing NoSymbol entries.
+ for len(ks) > 0 && ks[len(ks)-1] == 0 {
+ ks = ks[:len(ks)-1]
+ }
+
+ // Expand back to at least 4.
+ switch {
+ case len(ks) == 1:
+ ks = append(ks, 0, ks[0], 0)
+ case len(ks) == 2:
+ ks = append(ks, ks[0], ks[1])
+ case len(ks) == 3:
+ ks = append(ks, 0)
+ }
+
+ // Other silly expansion rules, only applied to basic ASCII since we
+ // don't have translation tables to Unicode here for brevity.
+ if ks[1] == 0 {
+ ks[1] = ks[0]
+ if ks[0] >= 'A' && ks[0] <= 'Z' ||
+ ks[0] >= 'a' && ks[0] <= 'z' {
+ ks[0] = ks[0] | 32
+ ks[1] = ks[0] &^ 32
+ }
+ }
+
+ if ks[3] == 0 {
+ ks[3] = ks[2]
+ if ks[2] >= 'A' && ks[2] <= 'Z' ||
+ ks[2] >= 'a' && ks[2] <= 'z' {
+ ks[2] = ks[2] | 32
+ ks[3] = ks[2] &^ 32
+ }
+ }
+
+ offset := 0
+ if e.State&km.modeSwitchMask != 0 {
+ offset += 2
+ }
+
+ shift := e.State&xproto.ModMaskShift != 0
+ lock := e.State&xproto.ModMaskLock != 0
+ switch {
+ case !shift && !lock:
+ result = ks[offset+0]
+ case !shift && lock:
+ if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' {
+ result = ks[offset+1]
+ } else {
+ result = ks[offset+0]
+ }
+ case shift && lock:
+ if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' {
+ result = ks[offset+1] &^ 32
+ } else {
+ result = ks[offset+1]
+ }
+ case shift:
+ result = ks[offset+1]
+ }
+ return
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ log.Fatalln("no filename given")
+ }
+
+ text, err := ioutil.ReadFile(os.Args[1])
+ if err != nil {
+ log.Fatalln(err)
+ }
+ lines := strings.Split(string(text), "\n")
+
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0xf0f0f0f0, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ /* KeymapNotify */ xproto.EventMaskKeymapState |
+ xproto.EventMaskExposure | xproto.EventMaskButtonPress,
+ uint32(mid)})
+
+ title := []byte("Viewer")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ blackid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreateSolidFill(X, blackid, render.Color{Alpha: 0xffff})
+
+ tr, err := newTextRenderer(X, goregular.TTF, &truetype.Options{
+ Size: 10,
+ DPI: float64(screen.WidthInPixels) /
+ float64(screen.WidthInMillimeters) * 25.4,
+ Hinting: font.HintingFull,
+ })
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ scroll := 0 // index of the top line
+
+ var w, h uint16
+ redraw := func() {
+ y, ascent, step := 5, tr.bounds.Max.Y.Ceil(),
+ tr.bounds.Max.Y.Ceil()-tr.bounds.Min.Y.Floor()
+ for _, line := range lines[scroll:] {
+ if uint16(y) >= h {
+ break
+ }
+ _ = tr.render(blackid, pid, 0, 0, 5, int16(y+ascent), line)
+ y += step
+ }
+
+ vis := float64(h-10) / float64(step)
+ if vis < float64(len(lines)) {
+ length := float64(step) * (vis + 1) * vis / float64(len(lines))
+ start := float64(step) * float64(scroll) * vis / float64(len(lines))
+
+ _ = render.FillRectangles(X, render.PictOpSrc, pid,
+ render.Color{Alpha: 0xffff}, []xproto.Rectangle{{
+ X: int16(w - 15), Y: int16(start),
+ Width: 15, Height: uint16(length + 10)}})
+ }
+ }
+
+ km, err := newKeyMapper(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+
+ case xproto.MappingNotifyEvent:
+ _ = km.update()
+
+ case xproto.KeyPressEvent:
+ _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
+
+ const pageJump = 40
+ switch km.decode(e) {
+ case ksEscape:
+ return
+ case ksUp:
+ if scroll >= 1 {
+ scroll--
+ }
+ case ksDown:
+ if scroll+1 < len(lines) {
+ scroll++
+ }
+ case ksPageUp:
+ if scroll >= pageJump {
+ scroll -= pageJump
+ }
+ case ksPageDown:
+ if scroll+pageJump < len(lines) {
+ scroll += pageJump
+ }
+ }
+
+ case xproto.ButtonPressEvent:
+ _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
+
+ switch e.Detail {
+ case xproto.ButtonIndex4:
+ if scroll > 0 {
+ scroll--
+ }
+ case xproto.ButtonIndex5:
+ if scroll+1 < len(lines) {
+ scroll++
+ }
+ }
+
+ case xproto.ExposeEvent:
+ // FIXME: The window's context haven't necessarily been destroyed.
+ if e.Count == 0 {
+ redraw()
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-window.go b/prototypes/xgb-window.go
new file mode 100644
index 0000000..3944fa4
--- /dev/null
+++ b/prototypes/xgb-window.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+ "math"
+ "math/rand"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ var visual xproto.Visualid
+ var depth byte
+ for _, i := range screen.AllowedDepths {
+ if i.Depth == 32 {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if v.Class == xproto.VisualClassTrueColor {
+ visual = v.VisualId
+ depth = i.Depth
+ break
+ }
+ }
+ }
+ }
+ if visual == 0 {
+ log.Fatalln("cannot find an RGBA TrueColor visual")
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBorderPixel|xproto.CwColormap,
+ []uint32{0, uint32(mid)})
+
+ // This could be included in CreateWindow parameters.
+ _ = xproto.ChangeWindowAttributes(X, wid,
+ xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ xproto.EventMaskExposure})
+
+ title := "Gradient"
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), []byte(title))
+
+ _ = xproto.MapWindow(X, wid)
+
+ cid, err := xproto.NewGcontextId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateGC(X, cid, xproto.Drawable(wid),
+ xproto.GcGraphicsExposures, []uint32{0})
+
+ blend := func(a, b uint32, ratio, gamma float64) uint32 {
+ iratio := 1 - ratio
+
+ fa := math.Pow(float64(a)/255, gamma)
+ fb := math.Pow(float64(b)/255, gamma)
+
+ return uint32(math.Pow(ratio*fa+iratio*fb, 1/gamma)*255) & 0xff
+ }
+
+ // TODO: We could show some text just like we intend to with xgb-xrender.go.
+
+ var w, h uint16
+ var start, end uint32 = 0xabcdef, 0x32ab54
+ gradient := func() {
+ ra, ga, ba := (start>>16)&0xff, (start>>8)&0xff, start&0xff
+ rb, gb, bb := (end>>16)&0xff, (end>>8)&0xff, end&0xff
+
+ var low, high uint16 = 50, h - 50
+ if high > h {
+ return
+ }
+
+ for y := low; y < high; y++ {
+ ratio := float64(y-low) / (float64(high) - float64(low))
+
+ rR := blend(ra, rb, ratio, 2.2)
+ gG := blend(ga, gb, ratio, 2.2)
+ bB := blend(ba, bb, ratio, 2.2)
+
+ _ = xproto.ChangeGC(X, cid, xproto.GcForeground,
+ []uint32{0xff000000 | rR<<16 | gG<<8 | bB})
+ _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
+ cid, []xproto.Point{
+ {X: 50, Y: int16(y)},
+ {X: int16(w / 2), Y: int16(y)},
+ })
+
+ rR = blend(ra, rb, ratio, 1)
+ gG = blend(ga, gb, ratio, 1)
+ bB = blend(ba, bb, ratio, 1)
+
+ _ = xproto.ChangeGC(X, cid, xproto.GcForeground,
+ []uint32{0xff000000 | rR<<16 | gG<<8 | bB})
+ _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
+ cid, []xproto.Point{
+ {X: int16(w / 2), Y: int16(y)},
+ {X: int16(w - 50), Y: int16(y)},
+ })
+ }
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+
+ case xproto.KeyPressEvent:
+ start = rand.Uint32() & 0xffffff
+ end = rand.Uint32() & 0xffffff
+ gradient()
+
+ case xproto.ExposeEvent:
+ gradient()
+ }
+ }
+
+}
diff --git a/prototypes/xgb-xrender.go b/prototypes/xgb-xrender.go
new file mode 100644
index 0000000..ad768b7
--- /dev/null
+++ b/prototypes/xgb-xrender.go
@@ -0,0 +1,397 @@
+package main
+
+// We could also easily use x/image/font/basicfont to load some glyphs into X,
+// relying on the fact that it is a vertical array of A8 masks. Though it only
+// supports ASCII and has but one size. Best just make a custom BDF loader,
+// those fonts have larger coverages and we would be in control. Though again,
+// they don't seem to be capable of antialiasing.
+
+import (
+ "fmt"
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ // golang.org/x/image/font/opentype cannot render yet but the API is
+ // more or less the same.
+ "github.com/golang/freetype"
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/math/fixed"
+ "image"
+ "image/draw"
+ "log"
+ "math/rand"
+)
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func glyphListBytes(buf []byte, runes []rune, size int) int {
+ b := 0
+ for _, r := range runes {
+ switch size {
+ default:
+ buf[b] = byte(r)
+ b += 1
+ case 2:
+ xgb.Put16(buf[b:], uint16(r))
+ b += 2
+ case 4:
+ xgb.Put32(buf[b:], uint32(r))
+ b += 4
+ }
+ }
+ return xgb.Pad(b)
+}
+
+// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32.
+func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int {
+ b := 0
+ buf[b] = len
+ b += 4
+ xgb.Put16(buf[b:], uint16(deltaX))
+ b += 2
+ xgb.Put16(buf[b:], uint16(deltaY))
+ b += 2
+ return xgb.Pad(b)
+}
+
+type xgbCookie interface{ Check() error }
+
+// TODO: We actually need a higher-level function that also keeps track of
+// and loads glyphs into the X server.
+// TODO: We also need a way to use kerning tables with this, inserting/removing
+// advance pixels between neighboring characters.
+
+// compositeString makes an appropriate render.CompositeGlyphs request,
+// assuming that glyphs equal Unicode codepoints.
+func compositeString(c *xgb.Conn, op byte, src, dst render.Picture,
+ maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16,
+ destX, destY int16, text string) xgbCookie {
+ runes := []rune(text)
+
+ var highest rune
+ for _, r := range runes {
+ if r > highest {
+ highest = r
+ }
+ }
+
+ size := 1
+ switch {
+ case highest > 1<<16:
+ size = 4
+ case highest > 1<<8:
+ size = 2
+ }
+
+ // They gave up on the XCB protocol API and we need to serialize explicitly.
+
+ // To spare us from caring about the padding, use the largest number lesser
+ // than 255 that is divisible by 4 (for size 2 and 4 the requirements are
+ // less strict but this works in the general case).
+ const maxPerChunk = 252
+
+ buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size)
+ b := 0
+
+ for len(runes) > maxPerChunk {
+ b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0)
+ b += glyphListBytes(buf[b:], runes[:maxPerChunk], size)
+ runes = runes[maxPerChunk:]
+ }
+ if len(runes) > 0 {
+ b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY)
+ b += glyphListBytes(buf[b:], runes, size)
+ }
+
+ switch size {
+ default:
+ return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 2:
+ return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 4:
+ return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ }
+}
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ var visual xproto.Visualid
+ var depth byte
+ for _, i := range screen.AllowedDepths {
+ if i.Depth == 32 {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if v.Class == xproto.VisualClassTrueColor {
+ visual = v.VisualId
+ depth = i.Depth
+ break
+ }
+ }
+ }
+ }
+ if visual == 0 {
+ log.Fatalln("cannot find an RGBA TrueColor visual")
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBorderPixel|xproto.CwColormap,
+ []uint32{0, uint32(mid)})
+
+ // This could be included in CreateWindow parameters.
+ _ = xproto.ChangeWindowAttributes(X, wid,
+ xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ xproto.EventMaskExposure})
+
+ title := []byte("Gradient")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ /*
+ rfilters, err := render.QueryFilters(X, xproto.Drawable(wid)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ filters := []string{}
+ for _, f := range rfilters.Filters {
+ filters = append(filters, f.Name)
+ }
+
+ log.Printf("filters: %v\n", filters)
+ */
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ /*
+ for _, pf := range pformats.Formats {
+ log.Printf("format %2d: depth %2d, RGBA %3x %3x %3x %3x\n",
+ pf.Id, pf.Depth,
+ pf.Direct.RedMask, pf.Direct.GreenMask, pf.Direct.BlueMask,
+ pf.Direct.AlphaMask)
+ }
+ */
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ // ...or just scan through pformats.Formats and look for matches, which is
+ // what XRenderFindStandardFormat in Xlib does as well as exp/shiny.
+
+ f, err := freetype.ParseFont(goregular.TTF)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // LCD subpixel rendering isn't supported. :(
+ opts := &truetype.Options{
+ Size: 10,
+ DPI: 96, // TODO: Take this from the screen or monitor.
+ Hinting: font.HintingFull,
+ }
+ face := truetype.NewFace(f, opts)
+ bounds := f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
+ (64.0 / 72.0)))
+
+ var rgbFormat render.Pictformat
+ for _, pf := range pformats.Formats {
+ // Hopefully. Might want to check byte order.
+ if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
+ rgbFormat = pf.Id
+ break
+ }
+ }
+
+ gsid, err := render.NewGlyphsetId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // NOTE: A depth of 24 will not work, the server always rejects it.
+ // Composite alpha doesn't make sense since golang/freetype can't use it.
+ // We use RGBA here just so that lines are padded to 32 bits.
+ _ = render.CreateGlyphSet(X, gsid, rgbFormat)
+
+ // NOTE: We could do gamma post-correction in higher precision if we
+ // implemented our own clone of the image.Image implementation.
+ nrgb := image.NewRGBA(image.Rect(
+ +bounds.Min.X.Floor(),
+ -bounds.Min.Y.Floor(),
+ +bounds.Max.X.Ceil(),
+ -bounds.Max.Y.Ceil(),
+ ))
+
+ for r := rune(32); r < 128; r++ {
+ dr, mask, maskp, advance, ok := face.Glyph(
+ fixed.P(0, 0) /* subpixel destination location */, r)
+ if !ok {
+ log.Println("skip")
+ continue
+ }
+
+ for i := 0; i < len(nrgb.Pix); i++ {
+ nrgb.Pix[i] = 0
+ }
+
+ draw.Draw(nrgb, dr, mask, maskp, draw.Src)
+
+ _ = render.AddGlyphs(X, gsid, 1, []uint32{uint32(r)},
+ []render.Glyphinfo{{
+ Width: uint16(nrgb.Rect.Size().X),
+ Height: uint16(nrgb.Rect.Size().Y),
+ X: int16(-bounds.Min.X.Floor()),
+ Y: int16(+bounds.Max.Y.Ceil()),
+ XOff: int16(advance.Ceil()),
+ YOff: int16(0),
+ }}, []byte(nrgb.Pix))
+ }
+
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Dithering is not supported. :(
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // Reserve an ID for the gradient.
+ gid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ whiteid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = render.CreateSolidFill(X, whiteid, render.Color{
+ Red: 0xffff,
+ Green: 0xffff,
+ Blue: 0xffff,
+ Alpha: 0xffff,
+ })
+
+ var from, to render.Color
+ var start, end uint32
+ recolor := func() {
+ start = rand.Uint32() & 0xffffff
+ from = render.Color{
+ Red: 0x101 * uint16((start>>16)&0xff),
+ Green: 0x101 * uint16((start>>8)&0xff),
+ Blue: 0x101 * uint16(start&0xff),
+ Alpha: 0xffff,
+ }
+
+ end = rand.Uint32() & 0xffffff
+ to = render.Color{
+ Red: 0x101 * uint16((end>>16)&0xff),
+ Green: 0x101 * uint16((end>>8)&0xff),
+ Blue: 0x101 * uint16(end&0xff),
+ Alpha: 0xffff,
+ }
+ }
+
+ var w, h uint16
+ gradient := func() {
+ if w < 100 || h < 100 {
+ return
+ }
+
+ // We could also use a transformation matrix for changes in size.
+ _ = render.CreateLinearGradient(X, gid,
+ render.Pointfix{F64ToFixed(0), F64ToFixed(0)},
+ render.Pointfix{F64ToFixed(0), F64ToFixed(float64(h) - 100)},
+ 2, []render.Fixed{F64ToFixed(0), F64ToFixed(1)},
+ []render.Color{from, to})
+
+ _ = render.Composite(X, render.PictOpSrc, gid, render.PictureNone, pid,
+ 0, 0, 0, 0, 50, 50, w-100, h-100)
+
+ _ = render.FreePicture(X, gid)
+
+ _ = compositeString(X, render.PictOpOver, whiteid, pid,
+ 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 100,
+ fmt.Sprintf("%#06x - %#06x", start, end))
+ _ = compositeString(X, render.PictOpOver, whiteid, pid,
+ 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 150,
+ "The quick brown fox jumps over the lazy dog.")
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+ recolor()
+
+ case xproto.KeyPressEvent:
+ recolor()
+ gradient()
+
+ case xproto.ExposeEvent:
+ gradient()
+ }
+ }
+}