204 Commits
v0.9 ... v0.9.3

Author SHA1 Message Date
11a6c7662e Update NEWS, bump release 2016-03-27 17:17:15 +02:00
dc71af9c31 degesch: fix two minor issues
- completely new unread markers could be created in active buffers
 - control characters confused word wrapping
2016-03-26 20:59:37 +01:00
f964495d1a degesch: don't wrap lines in pager 2016-03-26 16:41:55 +01:00
550a0419a6 degesch: detect //TRANSLIT support, use cp1252
Now BSDs should have it enabled as well.
2016-03-26 14:27:59 +01:00
9b12c830d1 degesch: remap goto-activity to M-a
weechat appears to use this key binding, so let's not reinvent it.
2016-03-26 13:11:28 +01:00
1e24d1d1b8 degesch: add partial matching /buffer goto 2016-03-26 13:00:10 +01:00
6292114c76 degesch: append message count in /buffer listing 2016-03-26 05:15:11 +01:00
e646afe5ae degesch: retain the unseen marker
Don't erase the message counts immediately.

Also make the marker visible in the pager.
2016-03-26 04:55:46 +01:00
410bcdcd78 degesch: phase 1 of word wrapping implementation 2016-03-26 04:52:05 +01:00
62962dc7ac Fix Travis CI notifications 2016-03-14 20:41:07 +01:00
a83ef111c8 Fix git commit tracking 2016-03-13 17:07:04 +01:00
90842c23a2 kike: fix daemonization on *BSD
Bump liberty.
2016-03-13 16:44:58 +01:00
1c9de9291b degesch: cleanup, fix no-tty mode FWIW
Removed the no color mode that couldn't even be enabled.

Not sure why we still support running without a proper terminal
but let's at least not make it crash for now.
2016-03-13 16:44:06 +01:00
e11ca7cc00 Use TMPDIR as a fallback if set 2016-03-13 00:59:28 +01:00
df395f32e5 Update NEWS, README 2016-03-12 23:02:59 +01:00
f96fa66168 degesch: add a --format switch 2016-03-12 14:28:17 +01:00
781a37c152 Don't link kike, ZyklonB against degesch libs 2016-03-10 22:27:09 +01:00
5a197162bf Fix manpage generation
The manpages could end up in a wrong directory.
2016-03-10 20:26:35 +01:00
d70f156a20 Update README 2016-03-10 20:25:06 +01:00
42d88f87f5 degesch: add unbound commands for buffer movement 2016-03-10 00:07:59 +01:00
a1c4a1ef3a degesch: fix binding to our own fns from inputrc 2016-03-10 00:06:28 +01:00
dc248b8840 degesch: add goto activity and highlight 2016-03-08 22:29:40 +01:00
09c7d9a65d degesch: fix mIRC color parsing 2016-03-08 01:59:51 +01:00
0f1fd2eb3a Update NEWS, cleanup 2016-03-07 23:43:47 +01:00
696273558e degesch: rewrite input layer
Now with less #ifdefs.
2016-03-07 22:52:56 +01:00
584d2f0295 degesch: use libffi to unify input callbacks
And fuck you both, Readline and Editline.
2016-03-06 18:12:12 +01:00
3304b718aa Try to use version information from git 2016-03-06 03:52:39 +01:00
10bdf90fe2 Don't force override compile flags 2016-03-06 03:50:35 +01:00
17804fa49b degesch: fix +=/-= to null config items 2016-03-05 19:15:40 +01:00
4b10ea7ab0 factoids: safer DB writes 2016-02-29 03:15:44 +01:00
fb0b0c4cf0 factoids: allow querying definitions by number 2016-02-29 03:11:33 +01:00
f492592735 factoids: look for duplicates case-insensitively 2016-02-29 02:50:53 +01:00
6190733079 degesch: mark a problem 2016-02-29 02:50:41 +01:00
676e6c20fa ZyklonB: add a factoids plugin 2016-02-27 22:29:19 +01:00
ed20322e5e ZyklonB: run plugins in a special work directory
Also small refactoring.
2016-02-20 00:37:57 +01:00
a275f9636c ZyklonB: revisit error handling 2016-02-20 00:01:54 +01:00
056e0a4765 Resolve tls_ca_{file,path} relative to config dir 2016-02-19 23:46:44 +01:00
798ed73a8c ZyklonB: fix segfault on total SOCKS failure 2016-02-12 04:11:33 +01:00
7be995f74a ZyklonB: avoid infinite reexec loops 2016-02-12 04:05:09 +01:00
06b03d336e degesch: fix segfault on sent messages w/o buffer 2016-02-12 04:04:35 +01:00
11519ee860 degesch: update screen size when terminal resumed
Apparently we don't receive the events when we give up the terminal.
2016-02-10 23:02:33 +01:00
03d5b27398 degesch: use mkstemp() with a safe umask 2016-02-09 13:52:56 +01:00
3315b16f79 degesch: log messages from /quote and plugins
That is, parse back all output messages and log based on that.
2016-02-09 05:10:41 +01:00
0c19a384f1 Fix typos 2016-02-09 04:50:51 +01:00
333ad2c981 degesch: allow changing the list of used CAPs
Bump liberty.
2016-02-01 21:57:43 +01:00
a850ee45f1 degesch: optimize buffer memory usage
We have approximately 5 formatter_items per buffer_line.  Let's assume
we're on a 64-bit machine.  Then there were (5 * 2) + 3 useless pointers
(104 bytes) as well as 5 * (4 + 4) = 40 bytes of wasted space because
of needless padding.  That's 144 bytes already.  Compared to that, this
change adds 16 bytes of overhead for an array sentinel, i.e. 128B less.

With a limit of 1000 lines per buffer, we've saved ~128kB per buffer
on completely useless data, and code complexity stays roughly the same.

All in all, memory usage for buffers should be about 50% lower.
2016-01-31 21:43:23 +01:00
10a264ec3d kike: add support for IRCv3.2 server-time 2016-01-31 21:43:23 +01:00
2ec6258ff3 last-fm.lua: don't use empty album names 2016-01-18 01:28:53 +01:00
f57664ddd0 degesch: add an assertion
It should never fail with current code.
2016-01-18 00:59:43 +01:00
773d14e740 degesch: disable TLS compression 2016-01-18 00:45:20 +01:00
221ae03b5c degesch: Lua: fix memory leak on load failure 2016-01-17 22:57:16 +01:00
588a696c68 degesch: lesser heap fragmentation 2016-01-17 22:15:48 +01:00
6db40c4503 Bump liberty 2016-01-17 04:43:43 +01:00
f070523085 Bump liberty 2016-01-16 06:30:08 +01:00
dac5c9df6d kike: more cleanup 2016-01-16 06:30:08 +01:00
ced2a57cfc kike: allow messages before protocol establishment
We can just queue them.
2016-01-16 06:30:08 +01:00
f36d66b0cb kike: asynchronous address resolution
As well as some refactoring and cleanup.
2016-01-16 06:30:08 +01:00
fdeb550ee0 degesch: fix backlog limit
It was effectively infinite.
2016-01-15 22:11:05 +01:00
c4a18ec8a7 degesch: fix and simplify screen handling
Now with less madness.
2016-01-15 05:40:20 +01:00
d0db1a6cdc degesch: enforce fullscreen buffers
Probably long overdue.

Now we actually look like something resembling a regular IRC client.
2016-01-15 05:12:03 +01:00
9333081178 degesch: option for fullscreen buffers 2016-01-15 05:09:42 +01:00
b7c9e8ca23 degesch: make backlog limit configurable 2016-01-15 05:09:42 +01:00
f39e2a4bc8 degesch: Lua: add autocomplete hooks 2016-01-15 02:39:10 +01:00
91f3bd60df degesch: Lua: finish the last-fm plugin 2016-01-14 04:13:03 +01:00
56858a97dd degesch: Lua: allow simulating user input
Also added UTF-8 validation to buffer:log() while I'm at it.
2016-01-14 03:34:29 +01:00
331d1842b9 Bump liberty, shuffle some code 2016-01-14 03:26:02 +01:00
19b09a8cec degesch: add a last-fm "now playing" plugin 2016-01-09 10:27:01 +01:00
32f719dec7 degesch: Lua: pass hostname to on_connected 2016-01-09 06:19:54 +01:00
0b92e9210c degesch: Lua: set sockets to nonblocking 2016-01-09 05:47:24 +01:00
092e9b5101 Bump liberty 2016-01-09 05:27:45 +01:00
faa0c989f8 degesch: Lua: actually allow filtering out input 2016-01-09 05:05:46 +01:00
53e72dd12d degesch: Lua: provide a traceback on load error 2016-01-09 05:01:50 +01:00
83c14ba264 degesch: Lua: fix plugin configuration names 2016-01-09 05:01:50 +01:00
64143a5957 degesch: Lua: fix luaL_ref() usage 2016-01-09 05:01:50 +01:00
aca153f575 degesch: Lua: fix configuration loading
Not the cleanest solution but it has to do for now.
2016-01-09 05:01:50 +01:00
79f46752d4 degesch: make sure newlines are output correctly 2016-01-08 08:40:40 +01:00
2a180ee084 degesch: Lua: finish implementation of connection 2016-01-07 22:49:53 +01:00
6754c59890 degesch: Lua: avoid resource leak
If a connector's on_success callback fails, we need to destroy the connection.
2016-01-07 22:49:53 +01:00
376bbea249 Factor out socket_io_try_{read,write}()
To be reused in Lua connection API.
2016-01-07 22:49:53 +01:00
a5ac0d24b8 degesch: fix handling of input editor death 2016-01-07 22:49:53 +01:00
cabab5f351 Fix a memory leak in SOCKS connector 2016-01-07 22:49:49 +01:00
1d3910fd8e degesch: fix switching of buffers by command
Readline used to erase the new buffer's contents.

Defer processing.
2016-01-07 22:49:49 +01:00
a259e96405 degesch: Lua: fix a resource leak 2016-01-06 00:23:54 +01:00
a7be2bf160 degesch: refactor Lua
And fix handling of nil returns from filter callbacks.
2016-01-05 23:19:28 +01:00
e1c7b8dcaf degesch: Lua: halfplement a connector wrapper
You can't do anything reasonable with the socket now.
2016-01-05 22:12:22 +01:00
00a1bdc707 Fix build of tests 2016-01-05 21:51:07 +01:00
e9b39a1ef7 degesch: Lua: allow arbitrary userdata properties 2016-01-04 23:14:38 +01:00
a227060383 degesch: Lua: use references for hook callbacks
Don't associate the callback with the full userdata object,
we'll need this for something else.
2016-01-04 22:24:05 +01:00
4832a99461 degesch: add basic autocomplete for /topic 2016-01-04 22:06:29 +01:00
0092c34568 Cleanup 2016-01-04 01:15:42 +01:00
aeb047260f Bump liberty, enable TLS SNI
Involves some rewrites to fit the new APIs.

SNI has been implemented Mostly just because we can, I don't think it's
widely in use and kike doesn't support this feature of the protocol either.
2016-01-04 01:12:42 +01:00
28fec6d4a6 ZyklonB: fix tls_ca_{path,file} config. options 2016-01-01 02:00:02 +01:00
1a73f1f1d7 degesch: fix a memory leak under libedit 2016-01-01 02:00:02 +01:00
312d0783cf Bump version 2015-12-31 23:46:17 +01:00
8564297e2a degesch: fix segfault on /quit under libedit 2015-12-31 23:42:43 +01:00
c015835d3a Update README 2015-12-31 05:07:28 +01:00
1d14abd875 Cleanup 2015-12-31 05:07:28 +01:00
74bed4bc02 degesch: Alt-Tab switch to the last buffer 2015-12-31 05:07:28 +01:00
8f229f41e1 degesch: avoid fileno() after fork()
It's not guaranteed to be async-signal-safe, which may matter once
we start using threads. And it's also cleaner to just pass the FD.
2015-12-31 05:07:28 +01:00
b4d6decc06 degesch: typos 2015-12-31 03:57:09 +01:00
04f87b7587 degesch: enable configuration in Lua plugins 2015-12-28 04:08:45 +01:00
b7dd384048 degesch: little step towards localisation
We don't use LC_MESSAGES, though, so it doesn't really matter as of now.

liberty currently isn't prepared for non-ASCII errors or filenames,
and just silently expects everything to be in the same all-compatible
encoding.  degesch further expects the encoding to be UTF-8.

All strings should ideally be converted to UTF-8 as soon as possible.
2015-12-25 21:36:56 +01:00
e101afab38 degesch: allow launching an editor for input
Useful for editing multiline text (such as making it single-line).

Some refactoring and cleanup.
2015-12-25 05:20:50 +01:00
37e9165548 degesch: better handling of terminal suspension 2015-12-25 05:03:02 +01:00
25bb7a978d degesch: refactoring 2015-12-25 05:02:58 +01:00
7d531a9bbf Bump liberty 2015-12-13 22:44:27 +01:00
1c009f394a Bump liberty 2015-12-11 03:01:25 +01:00
649ea0baf7 Refactor config schema initialization
Now the configuration module seems to be fit for inclusion in liberty.
2015-12-11 02:27:29 +01:00
de942e40ac Cleanup 2015-12-11 02:27:29 +01:00
5d3c2bea95 utm-filter.lua: filter out "gclid" as well 2015-12-10 21:48:04 +01:00
620418fa3b degesch: add a test for configuration 2015-12-10 21:26:17 +01:00
28e4bc1399 degesch: add more tests, bump liberty
The UTF-8 common prefix test discovered a bug in UTF-8 parsing.

Made $[1-9] in aliases insert nothing if there's no argument at that index.
2015-12-10 20:04:26 +01:00
a0becea2fc Update Travis CI and bump CMake 2015-12-09 21:07:01 +01:00
6a72c7382b Fix Travis CI 2015-12-09 21:07:01 +01:00
86d7b7aed5 degesch: add a unit test for message wrapping algo 2015-12-09 21:07:01 +01:00
07201b7bdc degesch: compactify word wrapping algorithm 2015-12-08 23:24:40 +01:00
2ae916fc1a degesch: clarify text wrapping algorithm 2015-12-08 22:11:11 +01:00
2ba8908024 degesch: fix timer hook resource leak 2015-11-29 17:55:35 +01:00
4a287a724e degesch: Lua: add a "server" property to buffers 2015-11-24 21:52:31 +01:00
87e1236b30 degesch: Lua: add a "buffer" property to servers 2015-11-24 21:42:08 +01:00
0044672b85 ping-timeout.lua: simplify 2015-11-24 20:45:51 +01:00
e921a619b0 degesch: readline fixups
When a new buffer was created automatically (channel was joined),
we didn't bother to erase the current line buffer.
2015-11-24 03:04:14 +01:00
25282cfe23 degesch: fix a segfault-inducing typo in hooks 2015-11-24 02:32:11 +01:00
8187bedcb6 degesch: add a URL cleaning Lua plugin 2015-11-24 02:26:07 +01:00
79140c3abc degesch: make auto-away less spammy 2015-11-23 08:33:58 +01:00
4d11be0b85 degesch: implement auto-away 2015-11-22 23:12:18 +01:00
b746c014aa Fix searching for Lua 5.3 on OpenBSD 2015-11-22 19:10:59 +01:00
f69edd6606 degesch: optimize prompt changes
We used to do lots of unnecessary redisplays.
2015-11-22 17:49:27 +01:00
385de6f4fe degesch: better terminal suspension
Don't print date changes while something else is using the terminal.
2015-11-22 16:43:21 +01:00
0fdffa0e50 degesch: fix hook debug logs
Obviously we can receive back the same pointer with different contents.

I just didn't think of that.
2015-11-22 03:01:38 +01:00
36c59ff375 Enable TCP_NODELAY 2015-11-22 02:12:52 +01:00
71f3532e04 degesch: add the first Lua plugin to distribution
This required separate plugin directories for both pluginized executables.
2015-11-21 22:47:52 +01:00
d135728424 degesch: pop() the Lua error in timer dispatch 2015-11-21 21:29:56 +01:00
2185af0b7d Update README 2015-11-21 21:23:59 +01:00
f22764ec56 degesch: update dependencies in README 2015-11-21 19:50:37 +01:00
02c7c6dcd6 degesch: export timers to Lua 2015-11-21 19:48:15 +01:00
364eb009ca degesch: hook implementation cleanup 2015-11-21 19:00:56 +01:00
d4cbc576e2 degesch: typos, cleanups 2015-11-21 19:00:56 +01:00
9bb9c9868c degesch: advertise Lua support 2015-11-21 14:09:34 +01:00
cd8e3d6d41 degesch: make Ctrl-L also fix window size 2015-11-21 14:09:34 +01:00
fa965a85e4 degesch: make /buffer with no arguments print list 2015-11-21 14:09:34 +01:00
59a4c356dd degesch: export input and IRC hooks 2015-11-21 14:09:34 +01:00
c912726f49 degesch: add ability to hook IRC and user input
We're going to make this available to the Lua API soon.
2015-11-21 14:09:34 +01:00
fbfe0ba18a degesch: add a stubbed Lua plugin loader 2015-11-21 14:09:33 +01:00
5ee210a5b7 degesch: stubplement plugins 2015-11-21 14:09:33 +01:00
5d55d7f6de degesch: refcountify "struct {buffer,server}" 2015-11-19 19:11:35 +01:00
b952fc1f6d degesch: extend weak pointers 2015-11-18 23:03:21 +01:00
89065e4d34 degesch: fix highlights 2015-11-17 00:06:48 +01:00
bc4b8ee19f Update NEWS 2015-11-15 16:32:52 +01:00
281ef2e93e degesch: split input text at newlines
This makes pasting multiline text possible again.
2015-11-15 15:56:33 +01:00
9b22d72fd1 Extend split_str() for multiple split chars 2015-11-15 15:56:10 +01:00
f11635ed7f degesch: better SIGTSTP handling 2015-11-15 15:36:03 +01:00
a1e47ca4c9 degesch: cleanup
Unnecessary oneliner function.
2015-11-15 01:48:10 +01:00
6c7a2ce3c8 degesch: unseen PMs show up as highlights
I used to miss them.
2015-11-15 01:43:00 +01:00
153d8c55d9 degesch: don't spam with all unseen messages
On high-traffic channels, it has shown to take quite some time.
2015-11-15 01:32:49 +01:00
d14bc2df53 degesch: have just one input buffer 2015-11-15 01:23:32 +01:00
d8299a1231 degesch: enable and use bracketed paste mode
urxvt, xterm and maybe others support quoting text pasted by the user
from clipboard, which prevents leading tabs from changing into
highlights.

The handling isn't perfect so far, just wrong in a different way, as
we mishandle newlines.
2015-11-15 01:07:12 +01:00
465c2e4082 degesch: mv input_insert{_c,}() 2015-11-15 01:07:09 +01:00
2a97c01215 degesch: make the libedit backend work again 2015-11-15 01:07:05 +01:00
152ba0847d Add a CMake target for clang-tidy 2015-11-13 09:22:48 +01:00
fe88e30bf5 degesch: fix beeping on "unimportant" PM events
Which in practice means stop beeping on quits in PM buffers.
2015-10-30 23:49:43 +01:00
a8a852d4b3 degesch: fix reconnect delays 2015-10-28 03:46:41 +01:00
e41f503202 degesch: add an /oper command
Mostly just because bitlbee suggested it to me and it didn't work.
2015-10-01 21:39:47 +02:00
762aaffecf degesch: make text attributes toggle formatting
Instead of just setting it on.

Fixes bitlbee.
2015-10-01 21:06:34 +02:00
99ac971b66 Little fixes to the README
Finally learned how to use this asciidoc{,tor} thing.
2015-09-27 01:12:03 +02:00
e75e840346 Convert README to AsciiDoc
So that it looks nice on GitHub.

Neither Markdown nor RST worked for me.
2015-09-27 00:38:20 +02:00
3d59a94554 Fix build instructions in README 2015-09-27 00:19:42 +02:00
f42ecedd42 Update release date 2015-09-25 17:12:29 +02:00
63a7980329 Bump version, add NEWS 2015-09-24 16:17:40 +02:00
bc54bf520d degesch: add Meta-H to open the full log file
As opposed to just the visible backlog.
2015-09-24 16:16:31 +02:00
11aaf1b325 degesch: fix logging of outgoing status messages
"/msg @#test test" would log "MSG(): test"
2015-09-24 15:41:39 +02:00
5ca07656a1 degesch: fix handling of status messages
That is, messages using the STATUSMSG feature.
2015-09-24 15:41:39 +02:00
f20c6fb28e degesch: fix logging of RPL_INVITING 2015-09-24 15:41:39 +02:00
1613e75a48 mv 'struct config_item'{_,}
Finally we can get rid of the trailing underscore.
2015-08-17 00:13:05 +02:00
abd892cbd7 Bump liberty 2015-08-17 00:13:05 +02:00
4ae95be9db degesch: add self to completion in server buffers 2015-08-13 00:23:56 +02:00
328bf9af1e degesch: display ERROR messages
So that the user knows he was killed.
2015-08-12 23:21:11 +02:00
ce83f8244c degesch: don't ignore data right before an EOF 2015-08-12 23:20:46 +02:00
8a8ff11887 degesch: don't use black for nicks on 256-color
Except for self.
2015-08-11 21:38:28 +02:00
131aee6f08 degesch: update comments 2015-08-10 23:24:57 +02:00
07f6d0b350 degesch: enable bright backgrounds on 8-color terms 2015-08-10 23:07:05 +02:00
1cc8656368 degesch: precompute the filtered color cube 2015-08-10 07:53:03 +02:00
4c81112840 degesch: show CTCPs to channels as such 2015-08-10 07:39:43 +02:00
5dda5661ae degesch: send after-connect joins more cleverly 2015-08-10 07:35:42 +02:00
628facf286 degesch: properly flush the read marker 2015-08-10 00:09:43 +02:00
7225b68f74 degesch: safer defaults for backlog helper 2015-08-09 15:05:49 +02:00
e188de5501 degesch: don't show joins etc. as new activity
It's mostly just spam that shouldn't get your attention.
2015-08-08 21:19:25 +02:00
cdf6544c94 degesch: use formatting in the backlog
It's a rather crude solution to just pipe the raw terminfo strings
to less but hey, it works.
2015-08-08 20:44:24 +02:00
a28528d260 degesch: add backlog/scrollback functionality
Finally!  I went with possibly the simplest solution, which is to
run less, instead of badly reimplementing its functionality.
2015-08-08 20:44:24 +02:00
27f185e8aa Update README 2015-08-07 00:14:44 +02:00
d207c90577 degesch: properly flush formatting resets 2015-08-06 23:58:42 +02:00
2afc9f99c3 degesch: better name resolution failure messages 2015-08-06 23:53:00 +02:00
4ab247ead0 degesch: fix /server usage 2015-08-06 23:27:35 +02:00
1dd464f35c degesch; fix handling of CTCP requests 2015-08-06 23:23:56 +02:00
955b3728a3 degesch: don't send PART on /close when not joined 2015-08-06 21:58:34 +02:00
aa77bc41d0 Fix library searching 2015-08-06 21:58:13 +02:00
5b208547c4 Bump liberty
Pulling in kqueue support.
2015-08-06 21:38:36 +02:00
c8890953b3 SOCKS: make use of the str_pack_* API
I forgot I had it.
2015-08-06 21:32:01 +02:00
cfc78ffdf0 Fix OpenBSD build 2015-07-30 18:29:12 +02:00
637a3d2bf7 More SSL -> TLS renaming 2015-07-28 20:31:42 +02:00
a912b3f28c degesch: use hopefully better colors for nicks
- exclude white from the 16-color range
 - use colors from the 256-color cube when available
2015-07-27 01:29:44 +02:00
27cd8b3a63 degesch: fix memory leak 2015-07-27 00:08:28 +02:00
2bde385dc7 degesch: order the nicknames in /names 2015-07-26 23:27:39 +02:00
74c9759932 degesch: make showing all prefixes optional 2015-07-26 22:44:34 +02:00
20 changed files with 6390 additions and 3167 deletions

View File

@@ -1,7 +1,9 @@
sudo: required
dist: trusty
language: c language: c
notifications: notifications:
irc: irc:
channels: "anathema.irc.so#anathema" channels: "irc.janouch.name#dev"
use_notice: true use_notice: true
skip_join: true skip_join: true
env: env:
@@ -25,15 +27,18 @@ compiler:
before_install: before_install:
# We need this PPA for a recent version of libedit # We need this PPA for a recent version of libedit
- sudo add-apt-repository ppa:ondrej/php5-5.6 -y - sudo add-apt-repository ppa:ondrej/php5-5.6 -y
# We need this PPA for Lua 5.3
- sudo add-apt-repository ppa:vbernat/haproxy-1.6 -y
- sudo apt-get update -qq - sudo apt-get update -qq
install: install:
- sudo apt-get install -y help2man libedit-dev expect - sudo apt-get install -y libncursesw5-dev libreadline-dev libedit-dev
liblua5.3-dev libffi-dev help2man expect
before_script: before_script:
- mkdir build - mkdir build
- cd build - cd build
script: script:
- cmake .. -DCMAKE_INSTALL_PREFIX=/usr - cmake .. -DCMAKE_INSTALL_PREFIX=/usr
-DWANT_READLINE=$readline -DWANT_LIBEDIT=$libedit -DWANT_READLINE=$readline -DWANT_LIBEDIT=$libedit
- make - make all test
- cpack -G DEB - cpack -G DEB
- ../test - ../test

View File

@@ -1,5 +1,5 @@
project (uirc3 C) project (uirc3 C)
cmake_minimum_required (VERSION 2.8.5) cmake_minimum_required (VERSION 2.8.11)
# Options # Options
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
@@ -8,43 +8,101 @@ option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
# Moar warnings # Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC)
# -Wunused-function is pretty annoying here, as everything is static # -Wunused-function is pretty annoying here, as everything is static
set (CMAKE_C_FLAGS "-std=c99 -Wall -Wextra -Wno-unused-function") set (CMAKE_C_FLAGS
"${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-function")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC)
# Version # Version
set (project_VERSION_MAJOR "0") set (project_version "0.9.3")
set (project_VERSION_MINOR "9")
set (project_VERSION_PATCH "0")
set (project_VERSION "${project_VERSION_MAJOR}") # Try to append commit ID if it follows a version tag. It might be nicer if
set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}") # we could also detect dirty worktrees but that's very hard to get right.
set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}") find_package (Git)
set (git_head "${PROJECT_SOURCE_DIR}/.git/HEAD")
if (GIT_FOUND AND EXISTS "${git_head}")
configure_file ("${git_head}" git-head.tag COPYONLY)
file (READ "${git_head}" git_head_content)
if (git_head_content MATCHES "^ref: ([^\r\n]+)")
set (git_ref "${PROJECT_SOURCE_DIR}/.git/${CMAKE_MATCH_1}")
if (EXISTS "${git_ref}")
configure_file ("${git_ref}" git-ref.tag COPYONLY)
endif (EXISTS "${git_ref}")
endif (git_head_content MATCHES "^ref: ([^\r\n]+)")
execute_process (COMMAND ${GIT_EXECUTABLE} describe --tags --match v*
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
RESULT_VARIABLE git_describe_result
OUTPUT_VARIABLE git_describe OUTPUT_STRIP_TRAILING_WHITESPACE)
if (NOT git_describe_result)
string (REGEX REPLACE "^v" "" project_version "${git_describe}")
endif (NOT git_describe_result)
endif (GIT_FOUND AND EXISTS "${git_head}")
# Dashes make filenames confusing and upset packaging software
string (REPLACE "-" "+" project_version_safe "${project_version}")
# Dependencies # Dependencies
find_package (Curses) set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake)
include (AddThreads)
find_package (PkgConfig REQUIRED) find_package (PkgConfig REQUIRED)
pkg_check_modules (libssl REQUIRED libssl libcrypto) pkg_check_modules (libssl REQUIRED libssl libcrypto)
pkg_check_modules (ncursesw ncursesw) list (APPEND project_libraries ${libssl_LIBRARIES})
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# iconv() doesn't have to be present in libc
# FIXME: detect if we need the library independently on the platform
list (APPEND project_libraries iconv)
# Need this for SIGWINCH; our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# -lpthread is only there for debugging (gdb & errno)
# -lrt is only for glibc < 2.17
list (APPEND project_libraries ${libssl_LIBRARIES} rt pthread)
include_directories (${libssl_INCLUDE_DIRS}) include_directories (${libssl_INCLUDE_DIRS})
link_directories (${libssl_LIBRARY_DIRS}) link_directories (${libssl_LIBRARY_DIRS})
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
include_directories (/usr/local/include)
link_directories (/usr/local/lib)
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# -lrt is only for glibc < 2.17
# -liconv may or may not be a part of libc
foreach (extra iconv rt)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND project_libraries ${extra_lib_${extra}})
endif (extra_lib_${extra})
endforeach (extra)
include (CheckCSourceRuns)
set (CMAKE_REQUIRED_LIBRARIES ${project_libraries})
get_property (CMAKE_REQUIRED_INCLUDES
DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY INCLUDE_DIRECTORIES)
CHECK_C_SOURCE_RUNS ("#include <iconv.h>
int main () { return iconv_open (\"UTF-8//TRANSLIT\", \"ISO-8859-1\")
== (iconv_t) -1; }" ICONV_ACCEPTS_TRANSLIT)
# Dependencies for degesch
pkg_check_modules (libffi REQUIRED libffi)
list (APPEND degesch_libraries ${libffi_LIBRARIES})
include_directories (${libffi_INCLUDE_DIRS})
link_directories (${libffi_LIBRARY_DIRS})
# FIXME: other Lua versions may be acceptable, don't know yet
pkg_search_module (lua lua53 lua5.3 lua-5.3 lua>=5.3)
option (WITH_LUA "Enable experimental support for Lua plugins" ${lua_FOUND})
if (WITH_LUA)
if (NOT lua_FOUND)
message (FATAL_ERROR "Lua library not found")
endif (NOT lua_FOUND)
list (APPEND degesch_libraries ${lua_LIBRARIES})
include_directories (${lua_INCLUDE_DIRS})
link_directories (${lua_LIBRARY_DIRS})
endif (WITH_LUA)
find_package (Curses)
pkg_check_modules (ncursesw ncursesw)
if (ncursesw_FOUND) if (ncursesw_FOUND)
list (APPEND project_libraries ${ncursesw_LIBRARIES}) list (APPEND degesch_libraries ${ncursesw_LIBRARIES})
include_directories (${ncursesw_INCLUDE_DIRS}) include_directories (${ncursesw_INCLUDE_DIRS})
elseif (CURSES_FOUND) elseif (CURSES_FOUND)
list (APPEND project_libraries ${CURSES_LIBRARY}) list (APPEND degesch_libraries ${CURSES_LIBRARY})
include_directories (${CURSES_INCLUDE_DIR}) include_directories (${CURSES_INCLUDE_DIR})
else (CURSES_FOUND) else (CURSES_FOUND)
message (SEND_ERROR "Curses not found") message (SEND_ERROR "Curses not found")
@@ -53,24 +111,27 @@ endif (ncursesw_FOUND)
if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)) if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
message (SEND_ERROR "You have to choose either GNU Readline or libedit") message (SEND_ERROR "You have to choose either GNU Readline or libedit")
elseif (WANT_READLINE) elseif (WANT_READLINE)
list (APPEND project_libraries readline) # OpenBSD's default readline is too old
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
include_directories (/usr/local/include/ereadline)
list (APPEND degesch_libraries ereadline)
else ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
list (APPEND degesch_libraries readline)
endif ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
elseif (WANT_LIBEDIT) elseif (WANT_LIBEDIT)
pkg_check_modules (libedit REQUIRED libedit) pkg_check_modules (libedit REQUIRED libedit)
list (APPEND project_libraries ${libedit_LIBRARIES}) list (APPEND degesch_libraries ${libedit_LIBRARIES})
include_directories (${libedit_INCLUDE_DIRS}) include_directories (${libedit_INCLUDE_DIRS})
endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)) endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
# Generate a configuration file # Generate a configuration file
if (WANT_READLINE) set (HAVE_READLINE "${WANT_READLINE}")
set (HAVE_READLINE 1) set (HAVE_EDITLINE "${WANT_LIBEDIT}")
endif (WANT_READLINE) set (HAVE_LUA "${WITH_LUA}")
if (WANT_LIBEDIT)
set (HAVE_EDITLINE 1)
endif (WANT_LIBEDIT)
include (GNUInstallDirs) include (GNUInstallDirs)
set (plugin_dir ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME}) # ZyklonB is currently an odd duck but degesch follows normal XDG rules
set (zyklonb_plugin_dir ${CMAKE_INSTALL_LIBDIR}/zyklonb/plugins)
configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)
include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
@@ -89,21 +150,59 @@ set_source_files_properties (${PROJECT_BINARY_DIR}/kike-replies.c
# Build # Build
add_executable (zyklonb zyklonb.c ${common_sources} ${common_headers}) add_executable (zyklonb zyklonb.c ${common_sources} ${common_headers})
target_link_libraries (zyklonb ${project_libraries}) target_link_libraries (zyklonb ${project_libraries})
add_threads (zyklonb)
add_executable (degesch degesch.c kike-replies.c add_executable (degesch degesch.c kike-replies.c
${common_sources} ${common_headers}) ${common_sources} ${common_headers})
target_link_libraries (degesch ${project_libraries}) target_link_libraries (degesch ${project_libraries} ${degesch_libraries})
add_threads (degesch)
add_executable (kike kike.c kike-replies.c ${common_sources} ${common_headers}) add_executable (kike kike.c kike-replies.c ${common_sources} ${common_headers})
target_link_libraries (kike ${project_libraries}) target_link_libraries (kike ${project_libraries})
add_threads (kike)
# Tests
function (make_tests_for target_name)
get_target_property (sources ${target_name} SOURCES)
get_target_property (libraries ${target_name} LINK_LIBRARIES)
get_target_property (options ${target_name} COMPILE_OPTIONS)
set (test test-${target_name})
add_executable (${test} ${sources})
target_link_libraries (${test} ${libraries})
set_target_properties (${test} PROPERTIES
COMPILE_DEFINITIONS TESTING
COMPILE_OPTIONS "${options}")
add_test (NAME ${test} COMMAND ${test})
endfunction (make_tests_for)
include (CTest)
if (BUILD_TESTING)
make_tests_for (degesch)
endif (BUILD_TESTING)
# Various clang-based diagnostics, loads of fake positives and spam
file (GLOB clang_tidy_sources *.c)
set (clang_tidy_checks misc-* readability-*
-readability-braces-around-statements
-readability-named-parameter)
string (REPLACE ";" "," clang_tidy_checks "${clang_tidy_checks}")
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_custom_target (clang-tidy
COMMAND clang-tidy -p ${PROJECT_BINARY_DIR} -checks=${clang_tidy_checks}
${clang_tidy_sources} 1>&2
USES_TERMINAL
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
# Installation # Installation
install (TARGETS zyklonb degesch kike DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS zyklonb degesch kike DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY plugins/zyklonb/
foreach (plugin coin eval script youtube ${plugins}) DESTINATION ${zyklonb_plugin_dir} USE_SOURCE_PERMISSIONS)
install (FILES plugins/${plugin} DESTINATION ${plugin_dir}) install (DIRECTORY plugins/degesch/
endforeach (plugin) DESTINATION ${CMAKE_INSTALL_DATADIR}/degesch/plugins)
# Generate documentation from program help # Generate documentation from program help
find_program (HELP2MAN_EXECUTABLE help2man) find_program (HELP2MAN_EXECUTABLE help2man)
@@ -124,26 +223,26 @@ endforeach (page)
add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES}) add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})
foreach (page ${project_MAN_PAGES}) foreach (page ${project_MAN_PAGES})
string (REGEX MATCH "\\.([0-9])" manpage_suffix "${page}") string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")
install (FILES "${page}" install (FILES "${page}"
DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}") DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")
endforeach (page) endforeach (page)
# CPack # CPack
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Experimental IRC client, daemon and bot") set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Experimental IRC client, daemon and bot")
set (CPACK_PACKAGE_VERSION ${project_VERSION}) set (CPACK_PACKAGE_VERSION "${project_version_safe}")
set (CPACK_PACKAGE_VENDOR "Premysl Janouch") set (CPACK_PACKAGE_VENDOR "Premysl Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>") set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p.janouch@gmail.com>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_GENERATOR "TGZ;ZIP") set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") "${PROJECT_NAME}-${project_version_safe}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}") set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_version_safe}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}") set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_version_safe}")
set (CPACK_SET_DESTDIR TRUE) set (CPACK_SET_DESTDIR TRUE)
include (CPack) include (CPack)

97
NEWS Normal file
View File

@@ -0,0 +1,97 @@
0.9.3 (2016-03-27) "Doesn't Even Suck"
* Use TLS Server Name Indication when connecting to servers
* degesch: now we erase the screen before displaying buffers
* degesch: implemented word wrapping in buffers
* degesch: added autocomplete for /topic
* degesch: Lua API was improved and extended
* degesch: added a basic last.fm "now playing" plugin
* degesch: backlog limit was made configurable
* degesch: allow changing the list of IRC capabilities to use if available
* degesch: optimize buffer memory usage
* degesch: added logging of messages sent from /quote and plugins
* degesch: M-! and M-a to go to the next buffer in order with
a highlight or new activity respectively
* degesch: added --format for previewing things like MOTD files
* degesch: added /buffer goto supporting case insensitive partial matches
* kike: add support for IRCv3.2 server-time
* ZyklonB: plugins now run in a dedicated data directory
* ZyklonB: added a factoids plugin
* Remote addresses are now resolved asynchronously
* Various bugfixes
0.9.2 (2015-12-31)
* degesch: added rudimentary support for Lua scripting
* degesch: added detection of pasting, so that it doesn't trigger other
keyboard shortcuts, such as for autocomplete
* degesch: added auto-away capability
* degesch: added an /oper command
* degesch: libedit backend works again
* degesch: added capability to edit the input line using VISUAL/EDITOR
* degesch: added Meta-Tab to switch to the last used buffer
* degesch: correctly respond to stopping and resuming (SIGTSTP)
* degesch: fixed decoding of text formatting
* degesch: unseen PMs now show up as highlights
* degesch: various bugfixes
0.9.1 (2015-09-25)
* All "ssl" options have been renamed to "tls"
* The project now builds on OpenBSD
* Pulled in kqueue support
* degesch: added backlog/scrollback functionality using less(1)
* degesch: made showing the entire set of channel mode user prefixes optional
* degesch: nicknames in /names are now ordered
* degesch: nicknames now use the 256-color terminal palette if available
* degesch: now we skip entries in the "addresses" list that can't be resolved
to an address, along with displaying a more helpful message
* degesch: joins, parts, nick changes and quits don't count as new buffer
activity anymore
* degesch: added Meta-H to open the full log file
* degesch: various bugfixes and little improvements
0.9.0 (2015-07-23)
* Initial release

127
README
View File

@@ -1,127 +0,0 @@
uirc3
=====
The unethical IRC trinity. This project consists of an experimental IRC client,
daemon, and bot. It's all you're ever going to need for chatting.
All of them have these potentially interesting properties:
- full IPv6 support
- TLS support, including client certificates
- minimal dependencies
- very compact and easy to hack on
- permissive license
degesch
-------
The IRC client. It is largely defined by being built on top of GNU Readline.
Its interface should however feel familiar for weechat or irssi users.
This is the youngest and largest application within the project. It has most of
the stuff you'd expect of an IRC client, such as being able to set up multiple
servers, powerful configuration system, integrated help, mIRC text formatting,
CTCP queries, automatic splitting of overlong messages, autocomplete, logging
to file, and command aliases.
kike
----
The IRC daemon. It is designed to be used as a regular user application rather
than a system-wide daemon. If all you want is a decent, minimal IRCd for
a small network of respectful users (or bots), or testing, this one will do it.
Notable features:
- TLS autodetection (why doesn't everyone have this?)
- IRCop authentication through TLS client certificates
- epoll support on Linux; it should be able to handle quite a number of users
- partial IRCv3 support
Not supported:
- server linking (which also means no services); I consider existing protocols
for this purpose ugly and tricky to implement correctly
- online changes to configuration; the config system from degesch could be used
- limits of almost any kind, just connections and mode +l
ZyklonB
-------
The IRC bot. It builds upon the concept of my other VitaminA IRC bot. The main
characteristic of these two bots is that they run plugins as coprocesses, which
allows for enhanced reliability and programming language freedom.
While originally intended to be a simple C99 rewrite of the original bot, which
was written in the GNU dialect of AWK, it fairly quickly became a playground
where I added everything that seemed nice, and it eventually got me into writing
the rest of this package.
Notable features:
- resilient against crashes, server disconnects and timeouts
- SOCKS support (even though socksify can add that easily to any program)
Building
--------
Build dependencies: CMake, pkg-config, help2man, awk, sh, liberty (included)
Runtime dependencies: openssl, curses (degesch),
readline or libedit >= 2013-07-12 (degesch)
$ git clone https://github.com/pjanouch/uirc3.git
$ git submodule init
$ git submodule update
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
-DWANT_READLINE=ON -DWANT_LIBEDIT=OFF
$ make
To install the application, you can do either the usual:
# make install
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i uirc3-*.deb
Note that for versions of CMake before 2.8.9, you need to prefix cpack with
`fakeroot' or file ownership will end up wrong.
Running
-------
`degesch' has in-program configuration. Just run it and read the instructions.
For the rest you might want to generate a configuration file:
$ zyklonb --write-default-config
$ kike --write-default-config
After making any necessary edits to the file (there are comments to aid you in
doing that), simply run the appropriate program with no arguments:
$ zyklonb
$ kike
`ZyklonB' stays running in the foreground, therefore I recommend launching it
inside a Screen or tmux session.
`kike', on the other hand, immediately forks into the background. Use the PID
file or something like `killall' if you want to terminate it. You can run it
as a `forking' type systemd user service.
Client Certificates
-------------------
`kike' uses SHA1 fingerprints of TLS client certificates to authenticate users.
To get the fingerprint from a certificate file in the required form, use:
$ openssl x509 -in public.pem -outform DER | sha1sum
Contributing and Support
------------------------
Use this project's GitHub to report any bugs, request features, or submit pull
requests. If you want to discuss this project, or maybe just hang out with
the developer, feel free to join me at irc://anathema.irc.so, channel #anathema.
Disclaimer
----------
I am not an antisemitist, I'm just being an offensive asshole with the naming.
And no, I'm not going to change the names.
License
-------
`uirc3' is written by Přemysl Janouch <p.janouch@gmail.com>.
You may use the software under the terms of the ISC license, the text of which
is included within the package, or, at your option, you may relicense the work
under the MIT or the Modified BSD License, as listed at the following site:
http://www.gnu.org/licenses/license-list.html

167
README.adoc Normal file
View File

@@ -0,0 +1,167 @@
uirc3
=====
:compact-option:
The unethical IRC trinity. This project consists of an experimental IRC client,
daemon, and bot. It's all you're ever going to need for chatting, as long as
you can make do with minimalist software.
All of them have these potentially interesting properties:
- full IPv6 support
- TLS support, including client certificates
- lean on dependencies (with the exception of 'degesch')
- compact and arguably easy to hack on
- permissive license
degesch
-------
The IRC client. It is largely defined by being built on top of GNU Readline
that has been hacked to death. Its interface should feel somewhat familiar for
weechat or irssi users.
This is the largest application within the project. It has most of the stuff
you'd expect of an IRC client, such as being able to set up multiple servers,
a powerful configuration system, integrated help, text formatting, CTCP queries,
automatic splitting of overlong messages, autocomplete, logging to file,
auto-away, command aliases and rudimentary support for Lua scripting.
kike
----
The IRC daemon. It is designed to be used as a regular user application rather
than a system-wide daemon. If all you want is a decent, minimal IRCd for
testing purposes or a small network of respectful users (or bots), this one will
do it just fine.
Notable features:
- TLS autodetection (why doesn't everyone have this?), using secure defaults
- IRCop authentication via TLS client certificates
- epoll/kqueue support; this means that it should be able to handle quite
a number of concurrent user connections
- partial IRCv3 support
Not supported:
- server linking (which also means no services); I consider existing protocols
for this purpose ugly and tricky to implement correctly; I've also found no
use for this feature yet
- online changes to configuration; the configuration system from degesch could
be used to implement this feature if needed
- limits of almost any kind, just connections and mode `+l`
ZyklonB
-------
The IRC bot. It builds upon the concept of my other VitaminA IRC bot. The main
characteristic of these two bots is that they run plugins as coprocesses, which
allows for enhanced reliability and programming language freedom.
While originally intended to be a simple rewrite of the original AWK bot in C,
it fairly quickly became a playground, and it eventually got me into writing
the rest of the package.
It survives crashes, server disconnects and timeouts, and also has native SOCKS
support (even though socksify can add that easily to any program).
Packages
--------
Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR, or from
openSUSE Build Service for the rest of mainstream distributions. Consult the
list of repositories and their respective links at:
https://build.opensuse.org/project/repositories/home:pjanouch:git
Building
--------
Build dependencies: CMake, pkg-config, help2man, awk, sh, liberty (included) +
Runtime dependencies: openssl +
Additionally for degesch: curses, libffi, lua >= 5.3 (optional),
readline >= 6.0 or libedit >= 2013-07-12
$ git clone --recursive https://github.com/pjanouch/uirc3.git
$ mkdir uirc3/build
$ cd uirc3/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug \
-DWANT_READLINE=ON -DWANT_LIBEDIT=OFF -DWANT_LUA=ON
$ make
To install the application, you can do either the usual:
# make install
Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB
# dpkg -i uirc3-*.deb
Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
`fakeroot` or file ownership will end up wrong.
Usage
-----
'degesch' has in-program configuration. Just run it and read the instructions.
For the rest you might want to generate a configuration file:
$ zyklonb --write-default-config
$ kike --write-default-config
After making any necessary edits to the file (there are comments to aid you in
doing that), simply run the appropriate program with no arguments:
$ zyklonb
$ kike
'ZyklonB' stays running in the foreground, therefore I recommend launching it
inside a Screen or tmux session.
'kike', on the other hand, immediately forks into the background. Use the PID
file or something like `killall` if you want to terminate it. You can run it
as a `forking` type systemd user service.
Client Certificates
-------------------
'kike' uses SHA1 fingerprints of TLS client certificates to authenticate users.
To get the fingerprint from a certificate file in the required form, use:
$ openssl x509 -in public.pem -outform DER | sha1sum
Custom Key Bindings in degesch
------------------------------
The default and preferred frontend used in 'degesch' is GNU Readline. This
means that you can change your bindings by editing '~/.inputrc'. For example:
....
# Preload with system-wide settings
$include /etc/inputrc
# Make M-left and M-right reorder buffers
$if degesch
"\e\e[C": move-buffer-right
"\e\e[D": move-buffer-left
$endif
....
Consult the source code and the GNU Readline manual for a list of available
functions. Also refer to the latter for the exact syntax of this file.
Beware that you can easily break the program if you're not careful.
Contributing and Support
------------------------
Use this project's GitHub to report any bugs, request features, or submit pull
requests. If you want to discuss this project, or maybe just hang out with
the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
Disclaimer
----------
I am not an antisemitist, I'm just being an offensive asshole with the naming.
And no, I'm not going to change the names.
License
-------
'uirc3' is written by Přemysl Janouch <p.janouch@gmail.com>.
You may use the software under the terms of the ISC license, the text of which
is included within the package, or, at your option, you may relicense the work
under the MIT or the Modified BSD License, as listed at the following site:
http://www.gnu.org/licenses/license-list.html

1539
common.c

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
#ifndef CONFIG_H #ifndef CONFIG_H
#define CONFIG_H #define CONFIG_H
#define PROGRAM_VERSION "${project_VERSION}" #define PROGRAM_VERSION "${project_version}"
#define PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/${plugin_dir}" #define ZYKLONB_PLUGIN_DIR "${CMAKE_INSTALL_PREFIX}/${zyklonb_plugin_dir}"
#cmakedefine HAVE_READLINE #cmakedefine HAVE_READLINE
#cmakedefine HAVE_EDITLINE #cmakedefine HAVE_EDITLINE
#cmakedefine HAVE_LUA
#cmakedefine01 ICONV_ACCEPTS_TRANSLIT
#endif // ! CONFIG_H #endif // ! CONFIG_H

6291
degesch.c

File diff suppressed because it is too large Load Diff

326
kike.c
View File

@@ -1,7 +1,7 @@
/* /*
* kike.c: the experimental IRC daemon * kike.c: the experimental IRC daemon
* *
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com> * Copyright (c) 2014 - 2016, Přemysl Janouch <p.janouch@gmail.com>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -34,7 +34,7 @@ enum { PIPE_READ, PIPE_WRITE };
// Just get rid of the crappiest ciphers available by default // Just get rid of the crappiest ciphers available by default
#define DEFAULT_CIPHERS "DEFAULT:!MEDIUM:!LOW" #define DEFAULT_CIPHERS "DEFAULT:!MEDIUM:!LOW"
static struct config_item g_config_table[] = static struct simple_config_item g_config_table[] =
{ {
{ "pid_file", NULL, "Path or name of the PID file" }, { "pid_file", NULL, "Path or name of the PID file" },
{ "server_name", NULL, "Server name" }, { "server_name", NULL, "Server name" },
@@ -44,9 +44,9 @@ static struct config_item g_config_table[] =
{ "bind_host", NULL, "Address of the IRC server" }, { "bind_host", NULL, "Address of the IRC server" },
{ "bind_port", "6667", "Port of the IRC server" }, { "bind_port", "6667", "Port of the IRC server" },
{ "ssl_cert", NULL, "Server TLS certificate (PEM)" }, { "tls_cert", NULL, "Server TLS certificate (PEM)" },
{ "ssl_key", NULL, "Server TLS private key (PEM)" }, { "tls_key", NULL, "Server TLS private key (PEM)" },
{ "ssl_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" }, { "tls_ciphers", DEFAULT_CIPHERS, "OpenSSL cipher list" },
{ "operators", NULL, "IRCop TLS cert. fingerprints" }, { "operators", NULL, "IRCop TLS cert. fingerprints" },
@@ -307,7 +307,8 @@ enum
IRC_CAP_MULTI_PREFIX = (1 << 0), IRC_CAP_MULTI_PREFIX = (1 << 0),
IRC_CAP_INVITE_NOTIFY = (1 << 1), IRC_CAP_INVITE_NOTIFY = (1 << 1),
IRC_CAP_ECHO_MESSAGE = (1 << 2), IRC_CAP_ECHO_MESSAGE = (1 << 2),
IRC_CAP_USERHOST_IN_NAMES = (1 << 3) IRC_CAP_USERHOST_IN_NAMES = (1 << 3),
IRC_CAP_SERVER_TIME = (1 << 4)
}; };
struct client struct client
@@ -330,17 +331,17 @@ struct client
struct poller_timer timeout_timer; ///< Connection seems to be dead struct poller_timer timeout_timer; ///< Connection seems to be dead
struct poller_timer kill_timer; ///< Hard kill timeout struct poller_timer kill_timer; ///< Hard kill timeout
bool initialized; ///< Has any data been received yet?
bool cap_negotiating; ///< Negotiating capabilities
bool registered; ///< The user has registered
bool closing_link; ///< Closing link
bool half_closed; ///< Closing link: conn. is half-closed
unsigned long cap_version; ///< CAP protocol version unsigned long cap_version; ///< CAP protocol version
unsigned caps_enabled; ///< Enabled capabilities unsigned caps_enabled; ///< Enabled capabilities
bool ssl_rx_want_tx; ///< SSL_read() wants to write unsigned initialized : 1; ///< Has any data been received yet?
bool ssl_tx_want_rx; ///< SSL_write() wants to read unsigned cap_negotiating : 1; ///< Negotiating capabilities
unsigned registered : 1; ///< The user has registered
unsigned closing_link : 1; ///< Closing link
unsigned half_closed : 1; ///< Closing link: conn. is half-closed
unsigned ssl_rx_want_tx : 1; ///< SSL_read() wants to write
unsigned ssl_tx_want_rx : 1; ///< SSL_write() wants to read
SSL *ssl; ///< SSL connection SSL *ssl; ///< SSL connection
char *ssl_cert_fingerprint; ///< Client certificate fingerprint char *ssl_cert_fingerprint; ///< Client certificate fingerprint
@@ -349,20 +350,23 @@ struct client
char *realname; ///< IRC realname (e-mail) char *realname; ///< IRC realname (e-mail)
char *hostname; ///< Hostname shown to the network char *hostname; ///< Hostname shown to the network
char *address; ///< Full address including port char *port; ///< Port of the peer as a string
char *address; ///< Full address
unsigned mode; ///< User's mode unsigned mode; ///< User's mode
char *away_message; ///< Away message char *away_message; ///< Away message
time_t last_active; ///< Last PRIVMSG, to get idle time time_t last_active; ///< Last PRIVMSG, to get idle time
struct str_map invites; ///< Channel invitations by operators struct str_map invites; ///< Channel invitations by operators
struct flood_detector antiflood; ///< Flood detector struct flood_detector antiflood; ///< Flood detector
struct async_getnameinfo *gni; ///< Backwards DNS resolution
struct poller_timer gni_timer; ///< Backwards DNS resolution timeout
}; };
static void static struct client *
client_init (struct client *self) client_new (void)
{ {
memset (self, 0, sizeof *self); struct client *self = xcalloc (1, sizeof *self);
self->socket_fd = -1; self->socket_fd = -1;
str_init (&self->read_buffer); str_init (&self->read_buffer);
str_init (&self->write_buffer); str_init (&self->write_buffer);
@@ -371,10 +375,11 @@ client_init (struct client *self)
flood_detector_init (&self->antiflood, 10, 20); flood_detector_init (&self->antiflood, 10, 20);
str_map_init (&self->invites); str_map_init (&self->invites);
self->invites.key_xfrm = irc_strxfrm; self->invites.key_xfrm = irc_strxfrm;
return self;
} }
static void static void
client_free (struct client *self) client_destroy (struct client *self)
{ {
if (!soft_assert (self->socket_fd == -1)) if (!soft_assert (self->socket_fd == -1))
xclose (self->socket_fd); xclose (self->socket_fd);
@@ -389,10 +394,16 @@ client_free (struct client *self)
free (self->realname); free (self->realname);
free (self->hostname); free (self->hostname);
free (self->port);
free (self->address); free (self->address);
free (self->away_message); free (self->away_message);
flood_detector_free (&self->antiflood); flood_detector_free (&self->antiflood);
str_map_free (&self->invites); str_map_free (&self->invites);
if (self->gni)
async_cancel (&self->gni->async);
free (self);
} }
static void client_close_link (struct client *c, const char *reason); static void client_close_link (struct client *c, const char *reason);
@@ -666,7 +677,7 @@ server_context_init (struct server_context *self)
str_map_init (&self->config); str_map_init (&self->config);
self->config.free = free; self->config.free = free;
load_config_defaults (&self->config, g_config_table); simple_config_load_defaults (&self->config, g_config_table);
str_vector_init (&self->motd); str_vector_init (&self->motd);
self->catalog = (nl_catd) -1; self->catalog = (nl_catd) -1;
@@ -819,12 +830,25 @@ client_get_mode (struct client *self)
static void static void
client_send_str (struct client *c, const struct str *s) client_send_str (struct client *c, const struct str *s)
{ {
hard_assert (c->initialized && !c->closing_link); hard_assert (!c->closing_link);
size_t old_sendq = c->write_buffer.len; size_t old_sendq = c->write_buffer.len;
// 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 (c->caps_enabled & IRC_CAP_SERVER_TIME)
{
long milliseconds; char buf[32]; struct tm tm;
time_t now = unixtime_msec (&milliseconds);
if (soft_assert (strftime (buf, sizeof buf,
"%Y-%m-%dT%T", gmtime_r (&now, &tm))))
str_append_printf (&c->write_buffer,
"@time=%s.%03ldZ ", buf, milliseconds);
}
// TODO: kill the connection above some "SendQ" threshold (careful!) // TODO: kill the connection above some "SendQ" threshold (careful!)
str_append_data (&c->write_buffer, s->str, str_append_data (&c->write_buffer, s->str,
s->len > IRC_MAX_MESSAGE_LENGTH ? IRC_MAX_MESSAGE_LENGTH : s->len); MIN (s->len, IRC_MAX_MESSAGE_LENGTH));
str_append (&c->write_buffer, "\r\n"); str_append (&c->write_buffer, "\r\n");
// XXX: we might want to move this elsewhere, so that it doesn't get called // XXX: we might want to move this elsewhere, so that it doesn't get called
// as often; it's going to cause a lot of syscalls with epoll. // as often; it's going to cause a lot of syscalls with epoll.
@@ -891,9 +915,46 @@ client_unregister (struct client *c, const char *reason)
c->registered = false; c->registered = false;
} }
static void
client_kill (struct client *c, const char *reason)
{
struct server_context *ctx = c->ctx;
client_unregister (c, reason ? reason : "Client exited");
if (c->address)
// Only log the event if address resolution has finished
print_debug ("closed connection to %s (%s)", c->address,
reason ? reason : "");
if (c->ssl)
// Note that we might have already called this once, but that is fine
(void) SSL_shutdown (c->ssl);
xclose (c->socket_fd);
c->socket_fd = -1;
c->socket_event.closed = true;
poller_fd_reset (&c->socket_event);
client_cancel_timers (c);
LIST_UNLINK (ctx->clients, c);
ctx->n_clients--;
client_destroy (c);
irc_try_finish_quit (ctx);
}
static void static void
client_close_link (struct client *c, const char *reason) client_close_link (struct client *c, const char *reason)
{ {
// Let's just cut the connection, the client can try again later.
// We also want to avoid accidentally setting poller events before
// address resolution has finished.
if (!c->initialized)
{
client_kill (c, reason);
return;
}
if (!soft_assert (!c->closing_link)) if (!soft_assert (!c->closing_link))
return; return;
@@ -910,35 +971,6 @@ client_close_link (struct client *c, const char *reason)
client_set_kill_timer (c); client_set_kill_timer (c);
} }
static void
client_kill (struct client *c, const char *reason)
{
client_unregister (c, reason ? reason : "Client exited");
struct server_context *ctx = c->ctx;
if (c->ssl)
// Note that we might have already called this once, but that is fine
(void) SSL_shutdown (c->ssl);
xclose (c->socket_fd);
c->socket_event.closed = true;
poller_fd_reset (&c->socket_event);
client_cancel_timers (c);
print_debug ("closed connection to %s (%s)",
c->address, reason ? reason : "");
c->socket_fd = -1;
client_free (c);
LIST_UNLINK (ctx->clients, c);
ctx->n_clients--;
free (c);
irc_try_finish_quit (ctx);
}
static bool static bool
client_in_mask_list (const struct client *c, const struct str_vector *mask) client_in_mask_list (const struct client *c, const struct str_vector *mask)
{ {
@@ -991,6 +1023,7 @@ client_cancel_timers (struct client *c)
poller_timer_reset (&c->kill_timer); poller_timer_reset (&c->kill_timer);
poller_timer_reset (&c->timeout_timer); poller_timer_reset (&c->timeout_timer);
poller_timer_reset (&c->ping_timer); poller_timer_reset (&c->ping_timer);
poller_timer_reset (&c->gni_timer);
} }
static void static void
@@ -1002,9 +1035,8 @@ client_set_timer (struct client *c,
} }
static void static void
on_client_kill_timer (void *user_data) on_client_kill_timer (struct client *c)
{ {
struct client *c = user_data;
hard_assert (!c->initialized || c->closing_link); hard_assert (!c->initialized || c->closing_link);
client_kill (c, NULL); client_kill (c, NULL);
} }
@@ -1016,9 +1048,8 @@ client_set_kill_timer (struct client *c)
} }
static void static void
on_client_timeout_timer (void *user_data) on_client_timeout_timer (struct client *c)
{ {
struct client *c = user_data;
char *reason = xstrdup_printf char *reason = xstrdup_printf
("Ping timeout: >%u seconds", c->ctx->ping_interval); ("Ping timeout: >%u seconds", c->ctx->ping_interval);
client_close_link (c, reason); client_close_link (c, reason);
@@ -1026,9 +1057,8 @@ on_client_timeout_timer (void *user_data)
} }
static void static void
on_client_ping_timer (void *user_data) on_client_ping_timer (struct client *c)
{ {
struct client *c = user_data;
hard_assert (!c->closing_link); hard_assert (!c->closing_link);
client_send (c, "PING :%s", c->ctx->server_name); client_send (c, "PING :%s", c->ctx->server_name);
client_set_timer (c, &c->timeout_timer, c->ctx->ping_interval); client_set_timer (c, &c->timeout_timer, c->ctx->ping_interval);
@@ -1235,6 +1265,7 @@ irc_cap_table[] =
{ IRC_CAP_INVITE_NOTIFY, "invite-notify" }, { IRC_CAP_INVITE_NOTIFY, "invite-notify" },
{ IRC_CAP_ECHO_MESSAGE, "echo-message" }, { IRC_CAP_ECHO_MESSAGE, "echo-message" },
{ IRC_CAP_USERHOST_IN_NAMES, "userhost-in-names" }, { IRC_CAP_USERHOST_IN_NAMES, "userhost-in-names" },
{ IRC_CAP_SERVER_TIME, "server-time" },
}; };
static void static void
@@ -1247,7 +1278,7 @@ irc_handle_cap_ls (struct client *c, struct irc_cap_args *a)
c->cap_negotiating = true; c->cap_negotiating = true;
client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message" client_send (c, ":%s CAP %s LS :multi-prefix invite-notify echo-message"
" userhost-in-names", c->ctx->server_name, a->target); " userhost-in-names server-time", c->ctx->server_name, a->target);
} }
static void static void
@@ -1784,7 +1815,7 @@ mode_processor_do_user (struct mode_processor *self, int mode)
target, self->channel->name); target, self->channel->name);
else if (irc_modify_mode (&target_user->modes, mode, self->adding)) else if (irc_modify_mode (&target_user->modes, mode, self->adding))
{ {
str_append_c (self->output, self->mode_char); \ str_append_c (self->output, self->mode_char);
str_vector_add (self->output_params, client->nickname); str_vector_add (self->output_params, client->nickname);
} }
} }
@@ -3106,7 +3137,7 @@ irc_try_read (struct client *c)
} }
static bool static bool
irc_try_read_ssl (struct client *c) irc_try_read_tls (struct client *c)
{ {
if (c->ssl_tx_want_rx) if (c->ssl_tx_want_rx)
return true; return true;
@@ -3174,7 +3205,7 @@ irc_try_write (struct client *c)
} }
static bool static bool
irc_try_write_ssl (struct client *c) irc_try_write_tls (struct client *c)
{ {
if (c->ssl_rx_want_tx) if (c->ssl_rx_want_tx)
return true; return true;
@@ -3211,8 +3242,10 @@ irc_try_write_ssl (struct client *c)
return true; return true;
} }
// -----------------------------------------------------------------------------
static bool static bool
irc_autodetect_ssl (struct client *c) irc_autodetect_tls (struct client *c)
{ {
// Trivial SSL/TLS autodetection. The first block of data returned by // Trivial SSL/TLS autodetection. The first block of data returned by
// recv() must be at least three bytes long for this to work reliably, // recv() must be at least three bytes long for this to work reliably,
@@ -3251,7 +3284,7 @@ start:
} }
static bool static bool
client_initialize_ssl (struct client *c) client_initialize_tls (struct client *c)
{ {
const char *error_info = NULL; const char *error_info = NULL;
if (!c->ctx->ssl_ctx) if (!c->ctx->ssl_ctx)
@@ -3281,6 +3314,8 @@ error_ssl_1:
return false; return false;
} }
// -----------------------------------------------------------------------------
static void static void
on_client_ready (const struct pollfd *pfd, void *user_data) on_client_ready (const struct pollfd *pfd, void *user_data)
{ {
@@ -3288,7 +3323,7 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
if (!c->initialized) if (!c->initialized)
{ {
hard_assert (pfd->events == POLLIN); hard_assert (pfd->events == POLLIN);
if (irc_autodetect_ssl (c) && !client_initialize_ssl (c)) if (irc_autodetect_tls (c) && !client_initialize_tls (c))
{ {
client_kill (c, NULL); client_kill (c, NULL);
return; return;
@@ -3301,7 +3336,7 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
{ {
// Reads may want to write, writes may want to read, poll() may // Reads may want to write, writes may want to read, poll() may
// return unexpected things in `revents'... let's try both // return unexpected things in `revents'... let's try both
if (!irc_try_read_ssl (c) || !irc_try_write_ssl (c)) if (!irc_try_read_tls (c) || !irc_try_write_tls (c))
return; return;
} }
else if (!irc_try_read (c) || !irc_try_write (c)) else if (!irc_try_read (c) || !irc_try_write (c))
@@ -3336,6 +3371,7 @@ on_client_ready (const struct pollfd *pfd, void *user_data)
static void static void
client_update_poller (struct client *c, const struct pollfd *pfd) client_update_poller (struct client *c, const struct pollfd *pfd)
{ {
// We must not poll for writing when the connection hasn't been initialized
int new_events = POLLIN; int new_events = POLLIN;
if (c->ssl) if (c->ssl)
{ {
@@ -3346,7 +3382,7 @@ client_update_poller (struct client *c, const struct pollfd *pfd)
if (c->ssl_rx_want_tx) new_events &= ~POLLIN; if (c->ssl_rx_want_tx) new_events &= ~POLLIN;
if (c->ssl_tx_want_rx) new_events &= ~POLLOUT; if (c->ssl_tx_want_rx) new_events &= ~POLLOUT;
} }
else if (c->write_buffer.len) else if (c->initialized && c->write_buffer.len)
new_events |= POLLOUT; new_events |= POLLOUT;
hard_assert (new_events != 0); hard_assert (new_events != 0);
@@ -3354,6 +3390,43 @@ client_update_poller (struct client *c, const struct pollfd *pfd)
poller_fd_set (&c->socket_event, new_events); poller_fd_set (&c->socket_event, new_events);
} }
static void
client_finish_connection (struct client *c)
{
c->gni = NULL;
c->address = format_host_port_pair (c->hostname, c->port);
print_debug ("accepted connection from %s", c->address);
client_update_poller (c, NULL);
client_set_kill_timer (c);
}
static void
on_client_gni_resolved (int result, char *host, char *port, void *user_data)
{
struct client *c = user_data;
if (result)
print_debug ("%s: %s", "getnameinfo", gai_strerror (result));
else
{
free (c->hostname);
c->hostname = xstrdup (host);
(void) port;
}
poller_timer_reset (&c->gni_timer);
client_finish_connection (c);
}
static void
on_client_gni_timer (struct client *c)
{
async_cancel (&c->gni->async);
client_finish_connection (c);
}
static bool static bool
irc_try_fetch_client (struct server_context *ctx, int listen_fd) irc_try_fetch_client (struct server_context *ctx, int listen_fd)
{ {
@@ -3378,6 +3451,15 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
return false; return false;
} }
set_blocking (fd, false);
// A little bit questionable once the traffic gets high enough (IMO),
// but it reduces silly latencies that we don't need because we already
// do buffer our output
int yes = 1;
soft_assert (setsockopt (fd, IPPROTO_TCP, TCP_NODELAY,
&yes, sizeof yes) != -1);
if (ctx->max_connections != 0 && ctx->n_clients >= ctx->max_connections) if (ctx->max_connections != 0 && ctx->n_clients >= ctx->max_connections)
{ {
print_debug ("connection limit reached, refusing connection"); print_debug ("connection limit reached, refusing connection");
@@ -3387,20 +3469,16 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown"; char host[NI_MAXHOST] = "unknown", port[NI_MAXSERV] = "unknown";
int err = getnameinfo ((struct sockaddr *) &peer, peer_len, int err = getnameinfo ((struct sockaddr *) &peer, peer_len,
host, sizeof host, port, sizeof port, NI_NUMERICSERV); host, sizeof host, port, sizeof port, NI_NUMERICHOST | NI_NUMERICSERV);
if (err) if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
char *address = format_host_port_pair (host, port); struct client *c = client_new ();
print_debug ("accepted connection from %s", address);
struct client *c = xmalloc (sizeof *c);
client_init (c);
c->ctx = ctx; c->ctx = ctx;
c->opened = time (NULL); c->opened = time (NULL);
c->socket_fd = fd; c->socket_fd = fd;
c->hostname = xstrdup (host); c->hostname = xstrdup (host);
c->address = address; c->port = xstrdup (port);
c->last_active = time (NULL); c->last_active = time (NULL);
LIST_PREPEND (ctx->clients, c); LIST_PREPEND (ctx->clients, c);
ctx->n_clients++; ctx->n_clients++;
@@ -3410,20 +3488,29 @@ irc_try_fetch_client (struct server_context *ctx, int listen_fd)
c->socket_event.user_data = c; c->socket_event.user_data = c;
poller_timer_init (&c->kill_timer, &c->ctx->poller); poller_timer_init (&c->kill_timer, &c->ctx->poller);
c->kill_timer.dispatcher = on_client_kill_timer; c->kill_timer.dispatcher = (poller_timer_fn) on_client_kill_timer;
c->kill_timer.user_data = c; c->kill_timer.user_data = c;
poller_timer_init (&c->timeout_timer, &c->ctx->poller); poller_timer_init (&c->timeout_timer, &c->ctx->poller);
c->timeout_timer.dispatcher = on_client_timeout_timer; c->timeout_timer.dispatcher = (poller_timer_fn) on_client_timeout_timer;
c->timeout_timer.user_data = c; c->timeout_timer.user_data = c;
poller_timer_init (&c->ping_timer, &c->ctx->poller); poller_timer_init (&c->ping_timer, &c->ctx->poller);
c->ping_timer.dispatcher = on_client_ping_timer; c->ping_timer.dispatcher = (poller_timer_fn) on_client_ping_timer;
c->ping_timer.user_data = c; c->ping_timer.user_data = c;
set_blocking (fd, false); // Resolve the client's hostname first; this is a blocking operation that
client_update_poller (c, NULL); // depends on the network, so run it asynchronously with some timeout
client_set_kill_timer (c); c->gni = async_getnameinfo (&ctx->poller.common.async,
(const struct sockaddr *) &peer, peer_len, NI_NUMERICSERV);
c->gni->dispatcher = on_client_gni_resolved;
c->gni->user_data = c;
poller_timer_init (&c->gni_timer, &c->ctx->poller);
c->gni_timer.dispatcher = (poller_timer_fn) on_client_gni_timer;
c->gni_timer.user_data = c;
poller_timer_set (&c->gni_timer, 5000);
return true; return true;
} }
@@ -3510,7 +3597,7 @@ irc_initialize_ssl_ctx (struct server_context *ctx,
SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
// XXX: perhaps we should read the files ourselves for better messages // XXX: perhaps we should read the files ourselves for better messages
const char *ciphers = str_map_find (&ctx->config, "ssl_ciphers"); const char *ciphers = str_map_find (&ctx->config, "tls_ciphers");
if (!SSL_CTX_set_cipher_list (ctx->ssl_ctx, ciphers)) if (!SSL_CTX_set_cipher_list (ctx->ssl_ctx, ciphers))
error_set (e, "failed to select any cipher from the cipher list"); error_set (e, "failed to select any cipher from the cipher list");
else if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path)) else if (!SSL_CTX_use_certificate_chain_file (ctx->ssl_ctx, cert_path))
@@ -3531,33 +3618,33 @@ irc_initialize_ssl_ctx (struct server_context *ctx,
} }
static bool static bool
irc_initialize_ssl (struct server_context *ctx, struct error **e) irc_initialize_tls (struct server_context *ctx, struct error **e)
{ {
const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
const char *ssl_key = str_map_find (&ctx->config, "ssl_key"); const char *tls_key = str_map_find (&ctx->config, "tls_key");
// Only try to enable SSL support if the user configures it; it is not // Only try to enable SSL support if the user configures it; it is not
// a failure if no one has requested it. // a failure if no one has requested it.
if (!ssl_cert && !ssl_key) if (!tls_cert && !tls_key)
return true; return true;
if (!ssl_cert) if (!tls_cert)
error_set (e, "no TLS certificate set"); error_set (e, "no TLS certificate set");
else if (!ssl_key) else if (!tls_key)
error_set (e, "no TLS private key set"); error_set (e, "no TLS private key set");
if (!ssl_cert || !ssl_key) if (!tls_cert || !tls_key)
return false; return false;
bool result = false; bool result = false;
char *cert_path = resolve_filename char *cert_path = resolve_filename
(ssl_cert, resolve_relative_config_filename); (tls_cert, resolve_relative_config_filename);
char *key_path = resolve_filename char *key_path = resolve_filename
(ssl_key, resolve_relative_config_filename); (tls_key, resolve_relative_config_filename);
if (!cert_path) if (!cert_path)
error_set (e, "%s: %s", "cannot open file", ssl_cert); error_set (e, "%s: %s", "cannot open file", tls_cert);
else if (!key_path) else if (!key_path)
error_set (e, "%s: %s", "cannot open file", ssl_key); error_set (e, "%s: %s", "cannot open file", tls_key);
else else
result = irc_initialize_ssl_ctx (ctx, cert_path, key_path, e); result = irc_initialize_ssl_ctx (ctx, cert_path, key_path, e);
@@ -3711,51 +3798,6 @@ irc_initialize_server_name (struct server_context *ctx, struct error **e)
return true; return true;
} }
static bool
lock_pid_file (const char *path, struct error **e)
{
// When using XDG_RUNTIME_DIR, the file needs to either have its
// access time bumped every 6 hours, or have the sticky bit set
int fd = open (path, O_RDWR | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH /* 644 */ | S_ISVTX /* sticky */);
if (fd < 0)
{
error_set (e, "can't open `%s': %s", path, strerror (errno));
return false;
}
struct flock lock =
{
.l_type = F_WRLCK,
.l_start = 0,
.l_whence = SEEK_SET,
.l_len = 0,
};
if (fcntl (fd, F_SETLK, &lock))
{
error_set (e, "can't lock `%s': %s", path, strerror (errno));
xclose (fd);
return false;
}
struct str pid;
str_init (&pid);
str_append_printf (&pid, "%ld", (long) getpid ());
if (ftruncate (fd, 0)
|| write (fd, pid.str, pid.len) != (ssize_t) pid.len)
{
error_set (e, "can't write to `%s': %s", path, strerror (errno));
xclose (fd);
return false;
}
str_free (&pid);
// Intentionally not closing the file descriptor; it must stay alive
// for the entire life of the application
return true;
}
static bool static bool
irc_lock_pid_file (struct server_context *ctx, struct error **e) irc_lock_pid_file (struct server_context *ctx, struct error **e)
{ {
@@ -3764,7 +3806,7 @@ irc_lock_pid_file (struct server_context *ctx, struct error **e)
return true; return true;
char *resolved = resolve_filename (path, resolve_relative_runtime_filename); char *resolved = resolve_filename (path, resolve_relative_runtime_filename);
bool result = lock_pid_file (resolved, e); bool result = lock_pid_file (resolved, e) != -1;
free (resolved); free (resolved);
return result; return result;
} }
@@ -3946,6 +3988,8 @@ daemonize (struct server_context *ctx)
int tty = open ("/dev/null", O_RDWR); int tty = open ("/dev/null", O_RDWR);
if (tty != 0 || dup (0) != 1 || dup (0) != 2) if (tty != 0 || dup (0) != 1 || dup (0) != 2)
exit_fatal ("failed to reopen FD's: %s", strerror (errno)); exit_fatal ("failed to reopen FD's: %s", strerror (errno));
poller_post_fork (&ctx->poller);
} }
int int
@@ -3982,7 +4026,7 @@ main (int argc, char *argv[])
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS); exit (EXIT_SUCCESS);
case 'w': case 'w':
call_write_default_config (optarg, g_config_table); call_simple_config_write_default (optarg, g_config_table);
exit (EXIT_SUCCESS); exit (EXIT_SUCCESS);
default: default:
print_error ("wrong options"); print_error ("wrong options");
@@ -4007,7 +4051,7 @@ main (int argc, char *argv[])
irc_register_cap_handlers (&ctx); irc_register_cap_handlers (&ctx);
struct error *e = NULL; struct error *e = NULL;
if (!read_config_file (&ctx.config, &e)) if (!simple_config_update_from_file (&ctx.config, &e))
{ {
print_error ("error loading configuration: %s", e->message); print_error ("error loading configuration: %s", e->message);
error_free (e); error_free (e);
@@ -4019,7 +4063,7 @@ main (int argc, char *argv[])
ctx.signal_event.user_data = &ctx; ctx.signal_event.user_data = &ctx;
poller_fd_set (&ctx.signal_event, POLLIN); poller_fd_set (&ctx.signal_event, POLLIN);
if (!irc_initialize_ssl (&ctx, &e) if (!irc_initialize_tls (&ctx, &e)
|| !irc_initialize_server_name (&ctx, &e) || !irc_initialize_server_name (&ctx, &e)
|| !irc_initialize_motd (&ctx, &e) || !irc_initialize_motd (&ctx, &e)
|| !irc_initialize_catalog (&ctx, &e) || !irc_initialize_catalog (&ctx, &e)

Submodule liberty updated: 02708608a9...365aed456e

180
plugins/degesch/last-fm.lua Normal file
View File

@@ -0,0 +1,180 @@
--
-- last-fm.lua: "now playing" feature using the last.fm API
--
-- Dependencies: lua-cjson (from luarocks e.g.)
--
-- I call this style closure-oriented programming
--
-- Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- 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.
--
local cjson = require "cjson"
-- Setup configuration to load last.fm API credentials from
local user, api_key
degesch.setup_config {
user = {
type = "string",
comment = "last.fm username",
on_change = function (v) user = v end
},
api_key = {
type = "string",
comment = "last.fm API key",
on_change = function (v) api_key = v end
},
}
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- Generic error reporting
local report_error = function (buffer, error)
buffer:log ("last-fm error: " .. error)
end
-- Process data return by the server and extract the now playing song
local process = function (buffer, data, action)
-- There's no reasonable Lua package to parse HTTP that I could find
local s, e, v, status, message = string.find (data, "(%S+) (%S+) .+\r\n")
if not s then return "server returned unexpected data" end
if status ~= "200" then return status .. " " .. message end
local s, e = string.find (data, "\r\n\r\n")
if not s then return "server returned unexpected data" end
local parser = cjson.new ()
data = parser.decode (string.sub (data, e + 1))
if not data.recenttracks or not data.recenttracks.track then
return "invalid response" end
-- Need to make some sense of the XML automatically converted to JSON
local text_of = function (node)
if type (node) ~= "table" then return node end
return node["#text"] ~= "" and node["#text"] or nil
end
local name, artist, album
for i, track in ipairs (data.recenttracks.track) do
if track["@attr"] and track["@attr"].nowplaying then
if track.name then name = text_of (track.name) end
if track.artist then artist = text_of (track.artist) end
if track.album then album = text_of (track.album) end
end
end
if not name then
action (false)
else
local np = "\"" .. name .. "\""
if artist then np = np .. " by " .. artist end
if album then np = np .. " from " .. album end
action (np)
end
end
-- Set up the connection and make the request
local on_connected = function (buffer, c, host, action)
-- Buffer data in the connection object
c.data = ""
c.on_data = function (data)
c.data = c.data .. data
end
-- And process it after we receive everything
c.on_eof = function ()
error = process (buffer, c.data, action)
if error then report_error (buffer, error) end
c:close ()
end
c.on_error = function (e)
report_error (buffer, e)
end
-- Make the unencrypted HTTP request
local url = "/2.0/?method=user.getrecenttracks&user=" .. user ..
"&limit=1&api_key=" .. api_key .. "&format=json"
c:send ("GET " .. url .. " HTTP/1.1\r\n")
c:send ("User-agent: last-fm.lua\r\n")
c:send ("Host: " .. host .. "\r\n")
c:send ("Connection: close\r\n")
c:send ("\r\n")
end
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-- Avoid establishing more than one connection at a time
local running
-- Initiate a connection to last.fm servers
local make_request = function (buffer, action)
if not user or not api_key then
report_error (buffer, "configuration is incomplete")
return
end
if running then running.abort () end
running = degesch.connect ("ws.audioscrobbler.com", 80, {
on_success = function (c, host)
on_connected (buffer, c, host, action)
running = nil
end,
on_error = function (e)
report_error (buffer, e)
running = nil
end
})
end
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
local now_playing
local tell_song = function (buffer)
if now_playing == nil then
buffer:log ("last-fm: I don't know what you're listening to")
elseif not now_playing then
buffer:log ("last-fm: not playing anything right now")
else
buffer:log ("last-fm: now playing: " .. now_playing)
end
end
local send_song = function (buffer)
if not now_playing then
tell_song (buffer)
else
buffer:execute ("/me is listening to " .. now_playing)
end
end
-- Hook input to simulate new commands
degesch.hook_input (function (hook, buffer, input)
if input == "/np" then
make_request (buffer, function (np)
now_playing = np
send_song (buffer)
end)
elseif input == "/np?" then
make_request (buffer, function (np)
now_playing = np
tell_song (buffer)
end)
elseif input == "/np!" then
send_song (buffer)
else
return input
end
end)

View File

@@ -0,0 +1,33 @@
--
-- ping-timeout.lua: ping timeout readability enhancement plugin
--
-- Copyright (c) 2015, Přemysl Janouch <p.janouch@gmail.com>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- 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.
--
degesch.hook_irc (function (hook, server, line)
local start, timeout =
line:match ("^(:[^ ]* QUIT :Ping timeout:) (%d+) seconds$")
if not start then
return line
end
local minutes = timeout // 60
if minutes == 0 then
return line
end
local seconds = timeout % 60
return ("%s %d minutes, %d seconds"):format (start, minutes, seconds)
end)

View File

@@ -0,0 +1,63 @@
--
-- utm-filter.lua: filter out Google Analytics bullshit from URLs
--
-- Copyright (c) 2015, Přemysl Janouch <p.janouch@gmail.com>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- 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.
--
-- A list of useless URL parameters that don't affect page function
local banned = {
gclid = 1,
utm_source = 1,
utm_medium = 1,
utm_term = 1,
utm_content = 1,
utm_campaign = 1,
}
-- Go through a parameter list and throw out any banned elements
local do_args = function (args)
local filtered = {}
for part in args:gmatch ("[^&]+") do
if not banned[part:match ("^[^=]*")] then
table.insert (filtered, part)
end
end
return table.concat (filtered, "&")
end
-- Filter parameters in both the query and the fragment part of an URL
local do_single_url = function (url)
return url:gsub ('^([^?#]*)%?([^#]*)', function (start, query)
local clean = do_args (query)
return #clean > 0 and start .. "?" .. clean or start
end, 1):gsub ('^([^#]*)#(.*)', function (start, fragment)
local clean = do_args (fragment)
return #clean > 0 and start .. "#" .. clean or start
end, 1)
end
local do_text = function (text)
return text:gsub ('%f[%g]https?://%g+', do_single_url)
end
degesch.hook_irc (function (hook, server, line)
local start, message = line:match ("^(.* :)(.*)$")
return message and start .. do_text (message) or line
end)
degesch.hook_input (function (hook, buffer, input)
return do_text (input)
end)

177
plugins/zyklonb/factoids Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env perl
#
# ZyklonB factoids plugin
#
# Copyright 2016 Přemysl Janouch <p.janouch@gmail.com>
# See the file LICENSE for licensing information.
#
use strict;
use warnings;
use Text::Wrap;
# --- IRC protocol -------------------------------------------------------------
binmode STDIN; select STDIN; $| = 1; $/ = "\r\n";
binmode STDOUT; select STDOUT; $| = 1; $\ = "\r\n";
sub parse ($) {
chomp (my $line = shift);
return undef unless my ($nick, $user, $host, $command, $args) = ($line =~
qr/^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/o);
return {nick => $nick, user => $user, host => $host, command => $command,
args => defined $args ? [$args =~ /:?((?<=:).*|[^ ]+) */og] : []};
}
sub bot_print {
print "ZYKLONB print :${\shift}";
}
# --- Initialization -----------------------------------------------------------
my %config;
for my $name (qw(prefix)) {
print "ZYKLONB get_config :$name";
$config{$name} = (parse <STDIN>)->{args}->[0];
}
print "ZYKLONB register";
# --- Database -----------------------------------------------------------------
# Simple map of (factoid_name => [definitions]); all factoids are separated
# by newlines and definitions by carriage returns. Both disallowed in IRC.
sub db_load {
local $/ = "\n";
my ($path) = @_;
open my $db, "<", $path or return {};
my %entries;
while (<$db>) {
chomp;
my @defs = split "\r";
$entries{shift @defs} = \@defs;
}
\%entries
}
sub db_save {
local $\ = "\n";
my ($path, $ref) = @_;
my $path_new = "$path.new";
open my $db, ">", $path_new or die "db save failed: $!";
my %entries = %$ref;
print $db join "\r", ($_, @{$entries{$_}}) for keys %entries;
close $db;
rename $path_new, $path or die "db save failed: $!";
}
# --- Factoids -----------------------------------------------------------------
my $db_path = 'factoids.db';
my %db = %{db_load $db_path};
sub learn {
my ($respond, $input) = @_;
return &$respond("usage: <name> = <definition>")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*=\s*(.+?)\s*$/;
my ($name, $number, $definition) = ($1, $2, $3);
return &$respond("trailing numbers in names are disallowed")
if defined $2;
$db{$name} = [] unless exists $db{$name};
my $entries = $db{$name};
return &$respond("duplicate definition")
if grep { lc $_ eq lc $definition } @$entries;
push @$entries, $definition;
&$respond("saved as #${\scalar @$entries}");
db_save $db_path, \%db;
}
sub check_number {
my ($respond, $name, $number) = @_;
my $entries = $db{$name};
if ($number > @$entries) {
&$respond(qq/"$name" has only ${\scalar @$entries} definitions/);
} elsif (not $number) {
&$respond("number must not be zero");
} else {
return 1;
}
return 0;
}
sub forget {
my ($respond, $input) = @_;
return &$respond("usage: <name> <number>")
unless $input =~ /^([^=]+?)\s+(\d+)\s*$/;
my ($name, $number) = ($1, int($2));
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
return unless check_number $respond, $name, $number;
splice @$entries, --$number, 1;
&$respond("forgotten");
db_save $db_path, \%db;
}
sub whatis {
my ($respond, $input) = @_;
return &$respond("usage: <name> [<number>]")
unless $input =~ /^([^=]+?)(?:\s+(\d+))?\s*$/;
my ($name, $number) = ($1, $2);
return &$respond(qq/"$name" is undefined/)
unless exists $db{$name};
my $entries = $db{$name};
if (defined $number) {
return unless check_number $respond, $name, $number;
&$respond(qq/"$name" is #$number $entries->[$number - 1]/);
} else {
my $i = 1;
my $definition = join ", ", map { "#${\$i++} $_" } @{$entries};
&$respond(qq/"$name" is $definition/);
}
}
sub wildcard {
my ($respond, $input) = @_;
$input =~ /=/ ? learn(@_) : whatis(@_);
}
my %commands = (
'learn' => \&learn,
'forget' => \&forget,
'whatis' => \&whatis,
'??' => \&wildcard,
);
# --- Input loop ---------------------------------------------------------------
while (my $line = <STDIN>) {
my %msg = %{parse $line};
my @args = @{$msg{args}};
# This plugin only bothers to respond to PRIVMSG messages
next unless $msg{command} eq 'PRIVMSG' and @args >= 2
and my ($cmd, $input) = $args[1] =~ /^$config{prefix}(\S+)\s*(.*)/;
# So far the only reaction is a PRIVMSG back to the sender, so all the
# handlers need is a response callback and all arguments to the command
my ($target => $quote) = ($args[0] =~ /^[#+&!]/)
? ($args[0] => "$msg{nick}: ") : ($msg{nick} => '');
# Wrap all responses so that there's space for our prefix in the message
my $respond = sub {
local ($Text::Wrap::columns, $Text::Wrap::unexpand) = 400, 0;
my $start = "PRIVMSG $target :$quote";
print for split "\n", wrap $start, $start, shift;
};
&{$commands{$cmd}}($respond, $input) if exists($commands{$cmd});
}

288
zyklonb.c
View File

@@ -1,7 +1,7 @@
/* /*
* zyklonb.c: the experimental IRC bot * zyklonb.c: the experimental IRC bot
* *
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com> * Copyright (c) 2014 - 2016, Přemysl Janouch <p.janouch@gmail.com>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above * purpose with or without fee is hereby granted, provided that the above
@@ -19,12 +19,13 @@
#include "config.h" #include "config.h"
#define PROGRAM_NAME "ZyklonB" #define PROGRAM_NAME "ZyklonB"
#define PLUGIN_DIR ZYKLONB_PLUGIN_DIR
#include "common.c" #include "common.c"
// --- Configuration (application-specific) ------------------------------------ // --- Configuration (application-specific) ------------------------------------
static struct config_item g_config_table[] = static struct simple_config_item g_config_table[] =
{ {
{ "nickname", "ZyklonB", "IRC nickname" }, { "nickname", "ZyklonB", "IRC nickname" },
{ "username", "bot", "IRC user name" }, { "username", "bot", "IRC user name" },
@@ -32,11 +33,11 @@ static struct config_item g_config_table[] =
{ "irc_host", NULL, "Address of the IRC server" }, { "irc_host", NULL, "Address of the IRC server" },
{ "irc_port", "6667", "Port of the IRC server" }, { "irc_port", "6667", "Port of the IRC server" },
{ "ssl", "off", "Whether to use SSL" }, { "tls", "off", "Whether to use TLS" },
{ "ssl_cert", NULL, "Client SSL certificate (PEM)" }, { "tls_cert", NULL, "Client TLS certificate (PEM)" },
{ "ssl_verify", "on", "Whether to verify certificates" }, { "tls_verify", "on", "Whether to verify certificates" },
{ "ssl_ca_file", NULL, "OpenSSL CA bundle file" }, { "tls_ca_file", NULL, "OpenSSL CA bundle file" },
{ "ssl_ca_path", NULL, "OpenSSL CA bundle path" }, { "tls_ca_path", NULL, "OpenSSL CA bundle path" },
{ "autojoin", NULL, "Channels to join on start" }, { "autojoin", NULL, "Channels to join on start" },
{ "reconnect", "on", "Whether to reconnect on error" }, { "reconnect", "on", "Whether to reconnect on error" },
{ "reconnect_delay", "5", "Time between reconnecting" }, { "reconnect_delay", "5", "Time between reconnecting" },
@@ -153,7 +154,7 @@ bot_context_init (struct bot_context *self)
{ {
str_map_init (&self->config); str_map_init (&self->config);
self->config.free = free; self->config.free = free;
load_config_defaults (&self->config, g_config_table); simple_config_load_defaults (&self->config, g_config_table);
self->admin_re = NULL; self->admin_re = NULL;
self->irc_fd = -1; self->irc_fd = -1;
@@ -309,8 +310,52 @@ irc_get_boolean_from_config
if (set_boolean_if_valid (value, str)) if (set_boolean_if_valid (value, str))
return true; return true;
error_set (e, "invalid configuration value for `%s'", name); FAIL ("invalid configuration value for `%s'", name);
return false; }
static bool
irc_initialize_ca_set (SSL_CTX *ssl_ctx, const char *file, const char *path,
struct error **e)
{
ERR_clear_error ();
if (file || path)
{
if (SSL_CTX_load_verify_locations (ssl_ctx, file, path))
return true;
FAIL ("%s: %s", "failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
}
if (!SSL_CTX_set_default_verify_paths (ssl_ctx))
FAIL ("%s: %s", "couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
return true;
}
static bool
irc_initialize_ca (struct bot_context *ctx, struct error **e)
{
const char *ca_file = str_map_find (&ctx->config, "tls_ca_file");
const char *ca_path = str_map_find (&ctx->config, "tls_ca_path");
char *full_file = ca_file
? resolve_filename (ca_file, resolve_relative_config_filename) : NULL;
char *full_path = ca_path
? resolve_filename (ca_path, resolve_relative_config_filename) : NULL;
bool ok = false;
if (ca_file && !full_file)
error_set (e, "couldn't find the CA bundle file");
else if (ca_path && !full_path)
error_set (e, "couldn't find the CA bundle path");
else
ok = irc_initialize_ca_set (ctx->ssl_ctx, full_file, full_path, e);
free (full_file);
free (full_path);
return ok;
} }
static bool static bool
@@ -320,36 +365,14 @@ irc_initialize_ssl_ctx (struct bot_context *ctx, struct error **e)
SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); SSL_CTX_set_options (ctx->ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
bool verify; bool verify;
if (!irc_get_boolean_from_config (ctx, "ssl_verify", &verify, e)) if (!irc_get_boolean_from_config (ctx, "tls_verify", &verify, e))
return false; return false;
SSL_CTX_set_verify (ctx->ssl_ctx, SSL_CTX_set_verify (ctx->ssl_ctx,
verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL); verify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
const char *ca_file = str_map_find (&ctx->config, "ca_file");
const char *ca_path = str_map_find (&ctx->config, "ca_path");
struct error *error = NULL; struct error *error = NULL;
if (ca_file || ca_path) if (!irc_initialize_ca (ctx, &error))
{ {
if (SSL_CTX_load_verify_locations (ctx->ssl_ctx, ca_file, ca_path))
return true;
error_set (&error, "%s: %s",
"failed to set locations for the CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
if (!SSL_CTX_set_default_verify_paths (ctx->ssl_ctx))
{
error_set (&error, "%s: %s",
"couldn't load the default CA certificate bundle",
ERR_reason_error_string (ERR_get_error ()));
goto ca_error;
}
return true;
ca_error:
if (verify) if (verify)
{ {
error_propagate (e, error); error_propagate (e, error);
@@ -359,11 +382,12 @@ ca_error:
// Only inform the user if we're not actually verifying // Only inform the user if we're not actually verifying
print_warning ("%s", error->message); print_warning ("%s", error->message);
error_free (error); error_free (error);
}
return true; return true;
} }
static bool static bool
irc_initialize_ssl (struct bot_context *ctx, struct error **e) irc_initialize_tls (struct bot_context *ctx, struct error **e)
{ {
const char *error_info = NULL; const char *error_info = NULL;
ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ());
@@ -376,17 +400,17 @@ irc_initialize_ssl (struct bot_context *ctx, struct error **e)
if (!ctx->ssl) if (!ctx->ssl)
goto error_ssl_2; goto error_ssl_2;
const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); const char *tls_cert = str_map_find (&ctx->config, "tls_cert");
if (ssl_cert) if (tls_cert)
{ {
char *path = resolve_filename char *path = resolve_filename
(ssl_cert, resolve_relative_config_filename); (tls_cert, resolve_relative_config_filename);
if (!path) if (!path)
print_error ("%s: %s", "cannot open file", ssl_cert); print_error ("%s: %s", "cannot open file", tls_cert);
// XXX: perhaps we should read the file ourselves for better messages // XXX: perhaps we should read the file ourselves for better messages
else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM) else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)
|| !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM)) || !SSL_use_PrivateKey_file (ctx->ssl, path, SSL_FILETYPE_PEM))
print_error ("%s: %s", "setting the SSL client certificate failed", print_error ("%s: %s", "setting the TLS client certificate failed",
ERR_error_string (ERR_get_error (), NULL)); ERR_error_string (ERR_get_error (), NULL));
free (path); free (path);
} }
@@ -418,8 +442,7 @@ error_ssl_1:
// multiple errors on the OpenSSL stack. // multiple errors on the OpenSSL stack.
if (!error_info) if (!error_info)
error_info = ERR_error_string (ERR_get_error (), NULL); error_info = ERR_error_string (ERR_get_error (), NULL);
error_set (e, "%s: %s", "could not initialize SSL", error_info); FAIL ("%s: %s", "could not initialize TLS", error_info);
return false;
} }
static bool static bool
@@ -432,11 +455,8 @@ irc_establish_connection (struct bot_context *ctx,
int err = getaddrinfo (host, port, &gai_hints, &gai_result); int err = getaddrinfo (host, port, &gai_hints, &gai_result);
if (err) if (err)
{ FAIL ("%s: %s: %s", "connection failed",
error_set (e, "%s: %s: %s", "getaddrinfo", gai_strerror (err));
"connection failed", "getaddrinfo", gai_strerror (err));
return false;
}
int sockfd; int sockfd;
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
@@ -477,10 +497,7 @@ irc_establish_connection (struct bot_context *ctx,
freeaddrinfo (gai_result); freeaddrinfo (gai_result);
if (!gai_iter) if (!gai_iter)
{ FAIL ("connection failed");
error_set (e, "connection failed");
return false;
}
ctx->irc_fd = sockfd; ctx->irc_fd = sockfd;
return true; return true;
@@ -635,6 +652,9 @@ recovery_handler (int signum, siginfo_t *info, void *context)
"signal received", signal_name); "signal received", signal_name);
*g_startup_reason_location = buf; *g_startup_reason_location = buf;
// Avoid annoying resource intensive infinite loops by sleeping for a bit
(void) sleep (1);
// TODO: maybe pregenerate the path, see the following for some other ways // TODO: maybe pregenerate the path, see the following for some other ways
// that would be illegal to do from within a signal handler: // that would be illegal to do from within a signal handler:
// http://stackoverflow.com/a/1024937 // http://stackoverflow.com/a/1024937
@@ -1001,87 +1021,78 @@ is_valid_plugin_name (const char *name)
return true; return true;
} }
static bool static struct plugin *
plugin_load (struct bot_context *ctx, const char *name, struct error **e) plugin_launch (struct bot_context *ctx, const char *name, struct error **e)
{ {
const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir"); const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir");
if (!plugin_dir) if (!plugin_dir)
{ FAIL ("plugin directory not set");
error_set (e, "plugin directory not set");
return false;
}
if (!is_valid_plugin_name (name))
{
error_set (e, "invalid plugin name");
return false;
}
if (str_map_find (&ctx->plugins_by_name, name))
{
error_set (e, "the plugin has already been loaded");
return false;
}
int stdin_pipe[2]; int stdin_pipe[2];
if (pipe (stdin_pipe) == -1) if (pipe (stdin_pipe) == -1)
{ FAIL ("%s: %s", "pipe", strerror (errno));
error_set (e, "%s: %s: %s",
"failed to load the plugin", "pipe", strerror (errno));
goto fail_1;
}
int stdout_pipe[2]; int stdout_pipe[2];
if (pipe (stdout_pipe) == -1) if (pipe (stdout_pipe) == -1)
{ {
error_set (e, "%s: %s: %s", error_set (e, "%s: %s", "pipe", strerror (errno));
"failed to load the plugin", "pipe", strerror (errno)); goto fail_1;
goto fail_2;
} }
struct str work_dir;
str_init (&work_dir);
get_xdg_home_dir (&work_dir, "XDG_DATA_HOME", ".local/share");
str_append_printf (&work_dir, "/%s", PROGRAM_NAME);
if (!mkdir_with_parents (work_dir.str, e))
goto fail_2;
set_cloexec (stdin_pipe[1]); set_cloexec (stdin_pipe[1]);
set_cloexec (stdout_pipe[0]); set_cloexec (stdout_pipe[0]);
pid_t pid = fork (); pid_t pid = fork ();
if (pid == -1) if (pid == -1)
{ {
error_set (e, "%s: %s: %s", error_set (e, "%s: %s", "fork", strerror (errno));
"failed to load the plugin", "fork", strerror (errno)); goto fail_2;
goto fail_3;
} }
if (pid == 0) if (pid == 0)
{ {
// Redirect the child's stdin and stdout to the pipes // Redirect the child's stdin and stdout to the pipes
hard_assert (dup2 (stdin_pipe[0], STDIN_FILENO) != -1); if (dup2 (stdin_pipe[0], STDIN_FILENO) == -1
hard_assert (dup2 (stdout_pipe[1], STDOUT_FILENO) != -1); || dup2 (stdout_pipe[1], STDOUT_FILENO) == -1)
{
xclose (stdin_pipe[0]); print_error ("%s: %s: %s", "failed to load the plugin",
xclose (stdout_pipe[1]); "dup2", strerror (errno));
_exit (EXIT_FAILURE);
struct str pathname; }
str_init (&pathname); if (chdir (work_dir.str))
str_append (&pathname, plugin_dir); {
str_append_c (&pathname, '/'); print_error ("%s: %s: %s", "failed to load the plugin",
str_append (&pathname, name); "chdir", strerror (errno));
// Restore some of the signal handling
signal (SIGPIPE, SIG_DFL);
char *const argv[] = { pathname.str, NULL };
execve (argv[0], argv, environ);
// We will collect the failure later via SIGCHLD
print_error ("%s: %s: %s",
"failed to load the plugin", "exec", strerror (errno));
_exit (EXIT_FAILURE); _exit (EXIT_FAILURE);
} }
xclose (stdin_pipe[0]); xclose (stdin_pipe[0]);
xclose (stdout_pipe[1]); xclose (stdout_pipe[1]);
set_blocking (stdout_pipe[0], false); // Restore some of the signal handling
set_blocking (stdin_pipe[1], false); signal (SIGPIPE, SIG_DFL);
char *argv[] = { xstrdup_printf ("%s/%s", plugin_dir, name), NULL };
execve (argv[0], argv, environ);
// We will collect the failure later via SIGCHLD
print_error ("%s: %s: %s", "failed to load the plugin",
"exec", strerror (errno));
_exit (EXIT_FAILURE);
}
str_free (&work_dir);
xclose (stdin_pipe[0]);
xclose (stdout_pipe[1]);
struct plugin *plugin = xmalloc (sizeof *plugin); struct plugin *plugin = xmalloc (sizeof *plugin);
plugin_init (plugin); plugin_init (plugin);
@@ -1090,6 +1101,32 @@ plugin_load (struct bot_context *ctx, const char *name, struct error **e)
plugin->name = xstrdup (name); plugin->name = xstrdup (name);
plugin->read_fd = stdout_pipe[0]; plugin->read_fd = stdout_pipe[0];
plugin->write_fd = stdin_pipe[1]; plugin->write_fd = stdin_pipe[1];
return plugin;
fail_2:
str_free (&work_dir);
xclose (stdout_pipe[0]);
xclose (stdout_pipe[1]);
fail_1:
xclose (stdin_pipe[0]);
xclose (stdin_pipe[1]);
return NULL;
}
static bool
plugin_load (struct bot_context *ctx, const char *name, struct error **e)
{
if (!is_valid_plugin_name (name))
FAIL ("invalid plugin name");
if (str_map_find (&ctx->plugins_by_name, name))
FAIL ("the plugin has already been loaded");
struct plugin *plugin;
if (!(plugin = plugin_launch (ctx, name, e)))
return false;
set_blocking (plugin->read_fd, false);
set_blocking (plugin->write_fd, false);
poller_fd_init (&plugin->read_event, &ctx->poller, plugin->read_fd); poller_fd_init (&plugin->read_event, &ctx->poller, plugin->read_fd);
plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable; plugin->read_event.dispatcher = (poller_fd_fn) on_plugin_readable;
@@ -1104,15 +1141,6 @@ plugin_load (struct bot_context *ctx, const char *name, struct error **e)
poller_fd_set (&plugin->read_event, POLLIN); poller_fd_set (&plugin->read_event, POLLIN);
return true; return true;
fail_3:
xclose (stdout_pipe[0]);
xclose (stdout_pipe[1]);
fail_2:
xclose (stdin_pipe[0]);
xclose (stdin_pipe[1]);
fail_1:
return false;
} }
static bool static bool
@@ -1121,10 +1149,7 @@ plugin_unload (struct bot_context *ctx, const char *name, struct error **e)
struct plugin *plugin = str_map_find (&ctx->plugins_by_name, name); struct plugin *plugin = str_map_find (&ctx->plugins_by_name, name);
if (!plugin) if (!plugin)
{ FAIL ("no such plugin is loaded");
error_set (e, "no such plugin is loaded");
return false;
}
plugin_zombify (plugin); plugin_zombify (plugin);
@@ -1444,7 +1469,7 @@ enum irc_read_result
}; };
static enum irc_read_result static enum irc_read_result
irc_fill_read_buffer_ssl (struct bot_context *ctx, struct str *buf) irc_fill_read_buffer_tls (struct bot_context *ctx, struct str *buf)
{ {
int n_read; int n_read;
start: start:
@@ -1608,7 +1633,7 @@ on_irc_readable (const struct pollfd *fd, struct bot_context *ctx)
struct str *buf = &ctx->read_buffer; struct str *buf = &ctx->read_buffer;
enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *) enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *)
= ctx->ssl = ctx->ssl
? irc_fill_read_buffer_ssl ? irc_fill_read_buffer_tls
: irc_fill_read_buffer; : irc_fill_read_buffer;
bool disconnected = false; bool disconnected = false;
while (true) while (true)
@@ -1662,8 +1687,10 @@ struct irc_socks_data
}; };
static void static void
irc_on_socks_connected (void *user_data, int socket) irc_on_socks_connected (void *user_data, int socket, const char *hostname)
{ {
(void) hostname;
struct irc_socks_data *data = user_data; struct irc_socks_data *data = user_data;
data->ctx->irc_fd = socket; data->ctx->irc_fd = socket;
data->succeeded = true; data->succeeded = true;
@@ -1721,6 +1748,8 @@ irc_establish_connection_socks (struct bot_context *ctx,
str_map_find (&ctx->config, "socks_password")); str_map_find (&ctx->config, "socks_password"));
while (data.polling) while (data.polling)
poller_run (poller); poller_run (poller);
if (!data.succeeded)
error_set (e, "connection failed");
} }
socks_connector_free (connector); socks_connector_free (connector);
@@ -1749,13 +1778,10 @@ irc_connect (struct bot_context *ctx, struct error **e)
// TODO: again, get rid of `struct error' in here. The question is: how // TODO: again, get rid of `struct error' in here. The question is: how
// do we tell our caller that he should not try to reconnect? // do we tell our caller that he should not try to reconnect?
if (!irc_host) if (!irc_host)
{ FAIL ("no hostname specified in configuration");
error_set (e, "no hostname specified in configuration");
return false;
}
bool use_ssl; bool use_tls;
if (!irc_get_boolean_from_config (ctx, "ssl", &use_ssl, e)) if (!irc_get_boolean_from_config (ctx, "tls", &use_tls, e))
return false; return false;
bool connected = socks_host bool connected = socks_host
@@ -1765,7 +1791,7 @@ irc_connect (struct bot_context *ctx, struct error **e)
if (!connected) if (!connected)
return false; return false;
if (use_ssl && !irc_initialize_ssl (ctx, e)) if (use_tls && !irc_initialize_tls (ctx, e))
{ {
xclose (ctx->irc_fd); xclose (ctx->irc_fd);
ctx->irc_fd = -1; ctx->irc_fd = -1;
@@ -1797,11 +1823,7 @@ parse_config (struct bot_context *ctx, struct error **e)
const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); const char *delay_str = str_map_find (&ctx->config, "reconnect_delay");
hard_assert (delay_str != NULL); // We have a default value for this hard_assert (delay_str != NULL); // We have a default value for this
if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10)) if (!xstrtoul (&ctx->reconnect_delay, delay_str, 10))
{ FAIL ("invalid configuration value for `%s'", "reconnect_delay");
error_set (e, "invalid configuration value for `%s'",
"reconnect_delay");
return false;
}
hard_assert (!ctx->admin_re); hard_assert (!ctx->admin_re);
const char *admin = str_map_find (&ctx->config, "admin"); const char *admin = str_map_find (&ctx->config, "admin");
@@ -1965,7 +1987,7 @@ main (int argc, char *argv[])
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS); exit (EXIT_SUCCESS);
case 'w': case 'w':
call_write_default_config (optarg, g_config_table); call_simple_config_write_default (optarg, g_config_table);
exit (EXIT_SUCCESS); exit (EXIT_SUCCESS);
default: default:
print_error ("wrong options"); print_error ("wrong options");
@@ -1988,7 +2010,7 @@ main (int argc, char *argv[])
bot_context_init (&ctx); bot_context_init (&ctx);
struct error *e = NULL; struct error *e = NULL;
if (!read_config_file (&ctx.config, &e) if (!simple_config_update_from_file (&ctx.config, &e)
|| !setup_recovery_handler (&ctx, &e)) || !setup_recovery_handler (&ctx, &e))
{ {
print_error ("%s", e->message); print_error ("%s", e->message);