134 Commits

Author SHA1 Message Date
dependabot-preview[bot]
eea24e3047 Bump golang.org/x/text from 0.3.3 to 0.3.4 (#79)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.3 to 0.3.4.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.3...v0.3.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-28 08:09:40 +10:00
dependabot-preview[bot]
e91312a468 Bump github.com/sirupsen/logrus from 1.6.0 to 1.7.0 (#78)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-15 16:51:36 +10:00
dependabot-preview[bot]
065a93d56f Bump github.com/imdario/mergo from 0.3.9 to 0.3.11 (#77)
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.9 to 0.3.11.
- [Release notes](https://github.com/imdario/mergo/releases)
- [Commits](https://github.com/imdario/mergo/compare/v0.3.9...v0.3.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-13 22:49:43 +10:00
dependabot-preview[bot]
fbbf36d1a1 Bump golang.org/x/text from 0.3.2 to 0.3.3 (#74)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.2 to 0.3.3.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.2...v0.3.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-17 14:40:05 +10:00
James Mills
3675251706 Refactored user flag/modes and fixed several data race bugs 2020-06-08 14:40:22 +10:00
James Mills
6f61b673a1 Refactor client 2020-06-08 14:20:21 +10:00
James Mills
4566b2021f Tidied up unused dependencies 2020-06-08 13:56:51 +10:00
James Mills
e7c5b96a6a Updated depednenices 2020-06-08 13:56:36 +10:00
James Mills
b18403ea71 Migrate to Github Actions Workflows 2020-06-08 13:47:06 +10:00
dependabot-preview[bot]
c9cbab6769 Bump github.com/stretchr/testify from 1.5.1 to 1.6.0 (#72)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.5.1...v1.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-01 11:21:26 +10:00
dependabot-preview[bot]
176aba3c99 Bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 (#68)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.8 to 2.3.0.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.8...v2.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-13 11:45:36 +10:00
dependabot-preview[bot]
d814c48dce Bump github.com/stretchr/testify from 1.3.0 to 1.5.1 (#67)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.3.0 to 1.5.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.3.0...v1.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-06 11:56:10 +10:00
dependabot-preview[bot]
3af82e3e8e Bump golang.org/x/text from 0.3.0 to 0.3.2 (#56)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.0 to 0.3.2.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.0...v0.3.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
2020-05-05 15:10:46 +10:00
dependabot-preview[bot]
8a8d7b1e97 Bump github.com/prometheus/client_golang from 0.9.2 to 0.9.4 (#58)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 0.9.2 to 0.9.4.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v0.9.2...v0.9.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
2020-05-05 15:09:36 +10:00
dependabot-preview[bot]
2ac33b7d2c Bump gopkg.in/yaml.v2 from 2.2.2 to 2.2.8 (#60)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.2 to 2.2.8.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.2...v2.2.8)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
2020-05-05 15:08:30 +10:00
dependabot-preview[bot]
a54031de9e Bump github.com/stretchr/testify from 1.2.2 to 1.5.1 (#62)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.2.2 to 1.5.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.2.2...v1.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
2020-05-05 15:08:14 +10:00
dependabot-preview[bot]
a49dea57d8 Bump github.com/imdario/mergo from 0.3.6 to 0.3.9 (#65)
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.6 to 0.3.9.
- [Release notes](https://github.com/imdario/mergo/releases)
- [Commits](https://github.com/imdario/mergo/compare/v0.3.6...v0.3.9)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
2020-05-05 15:07:20 +10:00
dependabot-preview[bot]
114c6aa80c Bump github.com/sirupsen/logrus from 1.2.0 to 1.6.0 (#66)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.2.0 to 1.6.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.2.0...v1.6.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-05 15:06:41 +10:00
James Mills
414c2fcf89 Added DependaBot config 2019-12-26 14:03:06 +10:00
James Mills
9d860692fa Update Drone CI config 2018-12-31 20:11:21 +10:00
James Mills
98cb66559a Migrate to Drone CI, Codecov and Go11Modules 2018-12-31 20:08:19 +10:00
James Mills
e14795818f Add related projects 2018-11-27 05:49:17 +10:00
James Mills
c394ea6735 Set theme jekyll-theme-architect 2018-11-27 05:41:33 +10:00
James Mills
c94884fb9f Add webhook for local CI 2018-05-19 14:00:00 -07:00
James Mills
784039998f Fixed local CI notify email settings 2018-05-19 13:35:25 -07:00
James Mills
6981b10763 Create PULL_REQUEST_TEMPLATE.md 2018-05-19 13:21:30 -07:00
James Mills
d170e01d38 Update issue templates 2018-05-19 13:20:24 -07:00
James Mills
5787059d11 Create CODE_OF_CONDUCT.md (#50) 2018-05-19 13:19:01 -07:00
James Mills
2e4ff30276 Add more recommended clients 2018-05-19 13:16:29 -07:00
James Mills
f18765a41a Add more recommended clients 2018-05-19 13:14:03 -07:00
James Mills
d23f7cf93d Reconfigure email notify rule for local CI 2018-05-19 12:51:15 -07:00
James Mills
6d64a46466 Updated 3rd-party vendored packages 2018-05-19 12:37:47 -07:00
James Mills
14ed3a6633 Fixed README 2018-05-19 10:25:44 -07:00
James Mills
cb46494733 Fixed build 2018-05-19 00:28:55 -07:00
James Mills
e3fea6c97b Fixed concurrent access to and data race on client.hasQuit 2018-05-19 00:01:14 -07:00
James Mills
9b70d25143 Add tests for metrics 2018-05-18 23:42:19 -07:00
James Mills
7a20037194 Disabled and commented out a recey test that still needs fixing 2018-05-18 23:31:03 -07:00
James Mills
5fa7214853 Fixed concurrent access to internl metrics maps 2018-05-18 23:30:43 -07:00
James Mills
e905b44fb4 Don't nilify the channel on closure (select will block indefinately) 2018-05-18 22:59:07 -07:00
James Mills
84a36a0095 Remove SIGQUIT and SIGHUP signals 2018-05-18 22:46:11 -07:00
James Mills
aa4907d8ae Add bench and profile targets 2018-05-18 22:34:09 -07:00
James Mills
facfcba232 Add grafana dashboard 2018-05-02 02:40:52 -07:00
James Mills
d3285748f9 Remove silly build token in version and add goreleaser config 2017-12-28 00:53:46 -08:00
Kevin Zita
283ef104a4 Aded support for HostMask user mode (+x) (#47) 2017-12-28 00:36:13 -08:00
James Mills
04d907d1e9 Fixed typo in README 2017-12-14 00:13:29 -08:00
James Mills
d74a6780fe Fixed writeloop goroutine leadk (#45) 2017-12-14 00:02:07 -08:00
James Mills
d7e9ef230a Refactored e2e integration tests 2017-12-06 20:59:04 -08:00
James Mills
75f224a7c0 Improves coverage of integreation tests to 40% (#42)
* Added TestChannel_PRIVMSG test

* Refactored integration testing framework for better timtouts and added TestChannel_InviteOnly test

* Try to fix TestChannel_PRIVMSG test

* Fuck it

* Added TestChannel_NoExternal test

* Added TestChannel_BadChannelKey and TestChannel_GoodChannelKey tests

* Bah humbut

* Update vendored 3rd-party deps

* Removed use of deadlock sync package

* Fix all tests :D yay

* Added some topic tests

* Add TestSASL
2017-12-06 02:19:27 -08:00
James Mills
caab002d51 Relase v1.6.4 2017-12-02 18:58:00 -08:00
James Mills
7ff892bba9 Added Drone CI test step 2017-12-02 18:56:53 -08:00
James Mills
233238b709 Adds a basic integration test suite framework (#38)
* Added a basic integration test suite framework

* Add TestConnect_RplWelcome test

* Update vendor 3rd-party deps for missing github.com/stretchr/testify/assert

* Add TestUser_PRIVMSG test

* Rename some tests

* Moar tests

* New test framework with better reliability
2017-11-28 02:15:30 -08:00
James Mills
59e0792db1 Fixed bad label value when bumping metrics for commands processed (#40) 2017-11-27 23:57:42 -08:00
James Mills
962b6645c1 Release v1.6.3 2017-11-27 19:18:58 -08:00
James Mills
cee8bf9957 Added support for multi-layer channel privacy (Public, Private and Secret) (#36) 2017-11-27 19:16:17 -08:00
James Mills
9d93bca179 Added support for measuring secure vs. non-secure registerd clients (#34) 2017-11-26 17:31:11 -08:00
James Mills
ccae795335 Fixed graceful shutdown (#32) 2017-11-26 17:30:53 -08:00
James Mills
862eb429d4 Update README.md 2017-11-26 15:26:12 -08:00
James Mills
9e075dde67 Fixed send on closed channel bug (#29) 2017-11-26 13:25:21 -08:00
James Mills
20be29bcef Fixed bug with RPL_ENDOFWHOIS (/WHOIS) response missing nick component (#27) 2017-11-26 10:42:14 -08:00
James Mills
34c3be0a88 Update README.md 2017-11-26 10:10:18 -08:00
Mike Taylor
be246a3bc4 minor typo fixes (#25) 2017-11-25 20:21:54 -08:00
James Mills
4fb452b2c0 Release v1.6.2 2017-11-25 20:19:05 -08:00
James Mills
d707382a78 Added support for user hostmask(s) / Hostname/IP Cloacks (#24) 2017-11-25 19:36:38 -08:00
James Mills
7620a3c282 Update README.md 2017-11-25 18:50:42 -08:00
James Mills
18a3e2f2c3 Update README.md 2017-11-25 18:47:15 -08:00
James Mills
d046a9863f Fixed /VERSION response (#22) 2017-11-25 17:57:09 -08:00
James Mills
a1450a81d6 Updated vendor 3rd-party packages (#20) 2017-11-25 16:42:35 -08:00
James Mills
d594386658 Fixed scripts/release.sh to correctly produce linux binaries for both amd64 and arm64 (#18) 2017-11-25 16:04:09 -08:00
James Mills
89b512fc76 Update README.md 2017-11-25 15:37:30 -08:00
James Mills
d01bb4fe57 Added support for network name and RPL_WELCOME to display network name (#14) 2017-11-25 15:22:31 -08:00
James Mills
2fef0feb5a Added Travis CI config and fixed some broken tests (#12) 2017-11-24 22:48:16 -08:00
James Mills
735458ffed Update README.md 2017-11-24 22:34:58 -08:00
Mike Taylor
02427bcb3f Issue #3 - unless the WHOIS request is from a user with the SecureConn flag, hide the hostmask (#11) 2017-11-24 22:29:58 -08:00
James Mills
bdcb4c21a5 Added contributors guideline (CONTRIBUTING.md) (#9) 2017-11-24 16:47:01 -08:00
James Mills
0e3be3f34c Ignore bin/ dir used to build binaries for release 2017-11-24 16:08:24 -08:00
James Mills
19e564ed2b Added scripts and release.sh script 2017-11-24 16:03:38 -08:00
James Mills
ef10282a37 Notify on successful Drone CI builds 2017-11-24 13:11:24 -08:00
James Mills
3a9d1fefc8 Update README.md 2017-11-23 01:34:38 -08:00
James Mills
f5d8f22220 Fix SecureChan (+Z) support and test it 2017-11-23 01:28:34 -08:00
Kevin Zita
062e2546ab Support Channel SecureOnly (+Z) (#6)
* First small changes...

* Added a check to see if the user is using a SecureOnly mode

* Tweaking for channel updates

* Almost working version

* Tweaking logic for CanSpeak()

* Fixing channel flags vs client flags.HasMode()s
2017-11-23 01:13:02 -08:00
James Mills
8f269b5201 Refactored basic SASL auth 2017-11-22 20:04:26 -08:00
James Mills
d33d60353c Fixed goroutine leak for writeloop 2017-11-22 19:59:14 -08:00
James Mills
9a5862287b Updated default config to fix reference to non-existent genpasswd (use an external tool) 2017-11-20 22:54:48 -08:00
James Mills
46d22a71b3 Update README.md 2017-11-20 20:13:13 -08:00
James Mills
4d97e035d2 Fixed some SASL issues @grawity found; Thank you 2017-11-20 01:25:49 -08:00
James Mills
41b6511cec AddRPL_WHOISLOGGEDIN and +r (registered) support for SASL 2017-11-20 01:03:46 -08:00
James Mills
1cde7c6902 Add support for very basic SASL auth 2017-11-19 22:57:22 -08:00
James Mills
edfd990d59 Include package name in the version output 2017-11-19 22:57:06 -08:00
James Mills
768f4f215a Release v1.6.0 2017-11-19 16:57:32 -08:00
James Mills
9601098872 Implement /WALLOPS and Global Notice(s) support 2017-11-19 16:50:17 -08:00
James Mills
d97fc927ad Fixed bug with /LIST skipping itereation on first private channel 2017-11-19 12:07:39 -08:00
James Mills
28ed5cc2c0 Updated README 2017-11-19 02:47:41 -08:00
James Mills
4ff06efab8 Fixed a bunch more race conditions and deadlocks 2017-11-19 02:36:20 -08:00
James Mills
51e1a93a99 Set default quantile objects to p50, p90, p95 and p99 2017-11-18 16:41:20 -08:00
James Mills
db4a9a864e Added overall client messages exchanged metric 2017-11-18 15:31:16 -08:00
James Mills
e333eb6029 Add overall client commands processed metric 2017-11-18 15:08:36 -08:00
James Mills
700c242e35 Add command processing time and client latency metrics 2017-11-18 13:38:53 -08:00
James Mills
c2512ca082 Fixed Dockerfile image build 2017-11-17 23:08:25 -08:00
James Mills
7e41395abd Missed one 2017-11-17 01:39:56 -08:00
James Mills
c1110f8b81 Refactor metrics and add channels gauge 2017-11-17 01:36:50 -08:00
James Mills
87663a4175 Update Drone CI config 2017-11-17 01:08:22 -08:00
James Mills
988820efb3 Add Docker stack file 2017-11-17 01:07:36 -08:00
James Mills
91212c3254 Set default metrics exporter port to 9314 2017-11-17 01:01:27 -08:00
James Mills
02b3525ef7 Add metrics (uptime, connections, clients) 2017-11-17 00:47:18 -08:00
James Mills
12d562c0fa Updated 3rd-party deps 2017-11-16 00:31:09 -08:00
James Mills
ec084f49ab Removed docopt cruft and simplify main 2017-11-16 00:30:31 -08:00
James Mills
97d5a1e199 Relase v1.5.4 2017-11-15 21:54:35 -08:00
James Mills
9e4115672c Fixed concurrent access to ChannelNameMap and ClientLookupSet preventing crashes 2017-11-15 21:38:20 -08:00
James Mills
af9207438a Remove cert localhost certs from repo 2017-11-14 22:56:27 -08:00
James Mills
4f9195e3e1 Vendor 3rd-party deps 2017-11-14 22:55:27 -08:00
James Mills
5e94c932ff Renamed project to eris 2017-11-14 22:49:31 -08:00
James Mills
6195810cd6 Fixed performance issues (single-threaded server) 2017-11-13 00:24:36 -08:00
James Mills
cc58f7ff62 Fixed client.CanSpeak() check to allow non-secure parties to message each other 2017-11-13 00:24:13 -08:00
James Mills
af8d3161f3 Removed use of SQLite 2017-11-12 23:36:17 -08:00
James Mills
0027d88e68 Code cleanup 2017-11-12 22:51:55 -08:00
James Mills
59b3cb1d7c Send RPC_NOTOPIC when there is no topic set for a channel 2017-11-12 16:11:25 -08:00
James Mills
5e945c863f Relase v1.5.3 2017-11-05 17:19:46 -08:00
James Mills
564f774a93 Add LUSERS command 2017-11-05 17:10:32 -08:00
James Mills
574a486ecd Add REHASH command 2017-11-05 14:47:54 -08:00
James Mills
8b52af0f17 Add Wallops and Wallopsf methods to server 2017-11-05 14:38:18 -08:00
James Mills
29082554b7 Update default config 2017-11-05 14:36:02 -08:00
James Mills
59f21700d6 Use logrus for logging 2017-11-05 01:11:47 -08:00
James Mills
6622b514c4 Add RPL_WHOISSERVER support 2017-11-05 01:19:17 -07:00
James Mills
e989b0111a strings: Disallow more broken nicknames 2017-11-05 00:32:14 -07:00
James Mills
78a7f3dde6 strings: Also explicitly disallow . in nicknames 2017-11-05 00:31:05 -07:00
James Mills
798e9f3cd2 strings: Don't allow nicks to start with dashes or 0-9 2017-11-05 00:29:55 -07:00
James Mills
9269ce4d52 strings: Restrict *? explicitly as they are used for mask matching 2017-11-05 00:15:35 -07:00
James Mills
500d14dafc tests: Start net tests 2017-11-05 00:08:12 -07:00
James Mills
cbbfd995f6 Set default channel modes to +nt 2017-11-05 00:02:56 -07:00
James Mills
1c24352a4f Require that server names must be hostnames, and nicks cannot be hostnames 2017-11-04 23:51:56 -07:00
James Mills
e26b8bb980 strings: Nicks and usernames can't contain ! or @ 2017-11-04 21:18:07 -07:00
James Mills
4a95857377 cap: Properly suspend registration for CAP negotiation 2017-11-04 20:26:38 -07:00
James Mills
83366c54f6 Fixed versioning 2017-11-04 19:08:59 -07:00
James Mills
de55aeff29 Release v1.5.2 2017-11-04 18:21:26 -07:00
52 changed files with 4896 additions and 865 deletions

8
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,8 @@
version: 1
update_configs:
- package_manager: "go:modules"
directory: "/"
update_schedule: "daily"
- package_manager: "docker"
directory: "/"
update_schedule: "weekly"

View File

@@ -1,30 +0,0 @@
workspace:
base: /go
path: src/github.com/prologic/ircd
pipeline:
build:
image: golang
commands:
- go get -d
- go build .
docker:
image: plugins/docker
repo: r.mills.io/prologic/ircd
registry: r.mills.io
secrets: [ docker_username, docker_password ]
notify:
image: drillster/drone-email
host: mail.mills.io
from: drone@mills.io
skip_verify: true
when:
status: [ changed, failure ]
secrets:
registry_username:
external: true
registry_password:
external: true

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

35
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Build
on:
push:
branches:
- master
pull_request:
jobs:
build:
name: Build
strategy:
matrix:
go-version:
- "1.12.x"
- "1.13.x"
- "1.14.x"
os:
- "ubuntu-latest"
- "macos-latest"
- "windows-latest"
runs-on: ${{ matrix.os }}
steps:
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
id: go
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: |
go build -v .
- name: Test
run: |
go test -v -race .

10
.gitignore vendored
View File

@@ -1,3 +1,9 @@
*~*
*~
*.db
ircd
*.bak
*.pem
/bin
/dist
/eris
/coverage.txt

31
.goreleaser.yml Normal file
View File

@@ -0,0 +1,31 @@
builds:
- binary: eris
flags: -tags "static_build"
ldflags: -w -X mail.Version={{.Version}} -X main.Commit={{.Commit}}
env:
- CGO_ENABLED=0
goos:
- darwin
- freebsd
- linux
- windows
goarch:
- i386
- amd64
- arm
- amd64
goarm:
- 6
- 7
sign:
artifacts: checksum
archive:
wrap_in_directory: true
format_overrides:
- goos: windows
format: zip
files:
- "*.pem"
- "*.yml"
- "LICENSE"
- "README.md"

29
.yamllint.yml Normal file
View File

@@ -0,0 +1,29 @@
---
yaml-files:
- '*.yaml'
- '*.yml'
- '.yamllint'
rules:
braces: enable
brackets: enable
colons: enable
commas: enable
comments: disable
comments-indentation: disable
document-end: disable
document-start:
level: warning
empty-lines: enable
empty-values: disable
hyphens: enable
indentation: enable
key-duplicates: enable
key-ordering: disable
line-length: disable
new-line-at-end-of-file: enable
new-lines: enable
octal-values: enable
quoted-strings: disable
trailing-spaces: enable
truthy: disable

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at prologic@shortcircuit.net.au. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

49
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,49 @@
# Welcome to the contributing guide for eris!
## Developers
- James Mills [@prologic](https://github.com/prologic) (*uthor / Maintainer*)
### Contributors
- [@bear](https://github.com/bear)
- [@DanielOaks](https://github.com/DanielOaks)
- [@kzisme](https://github.com/kzisme)
## New Features
* [Create an Issue](https://github.com/prologic/eris/issues/new)
* [Fork eris](https://github.com/prologic/eris#fork-destination-box)
```bash
$ git clone git@github.com:myuser/eris.git
```
* Create a new feature branch:
```bash
$ git checkout -b myfeature#issueN master
```
* Hack on your feature with your favorite editor
* Commit and Push your changes up:
```bash
$ git add -A
$ git commit -m "my fancy new feature"
$ git push -u origin my-feature
```
* Create a new [Pull Request](https://github.com/prologic/eris/compare/)
* Give the pull request an appropriate title possibly matching the issue
* In the pull request's description include the text `Closes #N` or `Fixes #N`
# Reporting Bugs
* File a new [Bug Report](https://github.com/prologic/eris/issues/new)
* Label it as a "Bug"
When describing your bug report; please be concise and as detailed as you can
so we can easily work out what the problem is. It's also very helpful if you
are able to provide a test case that repeatedly demonstrates the bug at hand.

View File

@@ -2,9 +2,8 @@
FROM golang:alpine AS build
ARG TAG
ARG BUILD
ENV APP ircd
ENV APP eris
ENV REPO prologic/$APP
RUN apk add --update git make build-base && \
@@ -12,19 +11,19 @@ RUN apk add --update git make build-base && \
WORKDIR /go/src/github.com/$REPO
COPY . /go/src/github.com/$REPO
RUN make TAG=$TAG BUILD=$BUILD build
RUN make TAG=$TAG build
# Runtime
FROM alpine
ENV APP ircd
ENV APP eris
ENV REPO prologic/$APP
LABEL ircd.app main
LABEL eris.app main
COPY --from=build /go/src/github.com/${REPO}/${APP} /${APP}
EXPOSE 6667/tcp 6697/tcp
ENTRYPOINT ["/ircd"]
CMD ["run"]
ENTRYPOINT ["/eris"]
CMD [""]

37
LICENSE
View File

@@ -1,22 +1,23 @@
The MIT License (MIT)
Copyright (C) 2017 James Mills
Copyright (C) 2014 Jeremy Latt
Copyright (c) 2017 James Mills
Copyright (c) 2014 Jeremy Latt
eris is covered by the MIT license::
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -2,10 +2,10 @@
CGO_ENABLED=0
COMMIT=`git rev-parse --short HEAD`
APP=ircd
APP=eris
PACKAGE=irc
REPO?=prologic/$(APP)
TAG?=latest
BUILD?=-dev
all: dev
@@ -16,17 +16,24 @@ deps:
@go get ./...
build: clean deps
@echo " -> Building $(TAG)$(BUILD)"
@echo "github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT)"
@echo " -> Building $(REPO) $(TAG)@$(COMMIT)"
@go build -tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X github.com/$(REPO)/$(APP).GitCommit=$(COMMIT) -X github.com/$(REPO)/$(APP).Build=$(BUILD)" .
-ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT)"
@echo "Built $$(./$(APP) -v)"
image:
@docker build --build-arg TAG=$(TAG) --build-arg BUILD=$(BUILD) -t $(REPO):$(TAG) .
@docker build --build-arg TAG=$(TAG) -t $(REPO):$(TAG) .
@echo "Image created: $(REPO):$(TAG)"
profile:
@go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench ./...
bench:
@go test -v -bench ./...
test:
@go test -v -cover -race $(TEST_ARGS)
@go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./...
clean:
@rm -rf $(APP)
@git clean -f -d -X

3
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,3 @@
<one line description here>
Fixes #xx

151
README.md
View File

@@ -1,4 +1,10 @@
# ircd - IRC Daemon
eris - IRC Server / Daemon written in Go
[![Build Status](https://cloud.drone.io/api/badges/prologic/eris/status.svg)](https://cloud.drone.io/prologic/eris)
[![CodeCov](https://codecov.io/gh/prologic/eris/branch/master/graph/badge.svg)](https://codecov.io/gh/prologic/eris)
[![Go Report Card](https://goreportcard.com/badge/prologic/eris)](https://goreportcard.com/report/prologic/eris)
[![GoDoc](https://godoc.org/github.com/prologic/eris?status.svg)](https://godoc.org/github.com/prologic/eris)
[![Sourcegraph](https://sourcegraph.com/github.com/prologic/eris/-/badge.svg)](https://sourcegraph.com/github.com/prologic/eris?badge)
> This project and repository is based off of [ergonomadic](https://github.com/edmund-huber/ergonomadic)
> and much of my original contributions were made in my [fork of ergonomadic](https://github.com/prologic/ergonomadic)
@@ -8,12 +14,31 @@
----
ircd is an IRC daemon written from scratch in Go.
> In philosophy and rhetoric, eristic (from Eris, the ancient Greek goddess
> of chaos, strife, and discord) refers to argument that aims to successfully
> dispute another's argument, rather than searching for truth. According to T.H.
From [Eris](https://en.wikipedia.org/wiki/Eris_(mythology))
and [Eristic](https://en.wikipedia.org/wiki/Eristic)
The connotation here is that IRC (*Internet Relay Chat*) is a place of chaos,
strife and discord. IRC is a place where you argue and get into arguments for
the sake of argument.
So `eris` is an IRC daemon written from scratch in Go to facilitate discord
and have arguments for the sake of argument!
Pull requests and issues are welcome.
Discussion at:
* host/port: irc.mills.io:6697 (*use SSL*)
* #lobby
* /server irc.mills.io +6697 (*use TLS/SSL*)
* /join #lobby
Or (**not recommended**):
* /server irc.mills.io (*default port 6667, non-TLS*)
* /join #lobby
## Features
@@ -28,32 +53,128 @@ Discussion at:
* SSL/TLS support
* Simple IRC operator privileges (*overrides most things*)
* Secure connection tracking (+z) and SecureOnly user mode (+Z)
* Secure channels (+Z)
* Three layers of channel privacy, Public, Private (+p) and Secret (s)
## Quick Start
```#!bash
$ go get github.com/prologic/eris
$ cat > ircd.yml <<EOF
network:
name: Test
server:
name: Test
listen:
- ":6667"
EOF
$ eris
```
If you want TLS (**recommended**) then:
```#!bash
$ go get github.com/prologic/mksslcert
$ mksslcert
```
This generates a self-signed cert `cert.pem` and `key.pem` into the `$PWD`.
Then add a `tlslisten` block to your config:
```#!yaml
server:
tlslisten:
":6697":
key: key.pem
cert: cert.pem
```
## Installation
```#!bash
$ go install github.com/prologic/ircd
$ ircd --help
$ go install github.com/prologic/eris
$ eris --help
```
## Configuration
See the example [ircd.yml](ircd.yml). Passwords are base64-encoded
bcrypted byte strings. You can generate them with the `genpasswd` subcommand.
bcrypted byte strings. You can generate them with the `mkpasswd` tool
from [prologic/mkpasswd](https://github.com/prologic/mkpasswd):
```#!bash
$ ircd genpasswd
$ go install github.com/prologic/mkpasswd
$ mkpasswd
```
## Running the server
Self-signed certificates can also be generated using the `mksslcert` tool
from [prologic/mksslcert](https://github.com/prologic/mksslcert):
```#!bash
$ ircd run
$ go install github.com/prologic/mksslcert
$ mksslcert
```
## Credits
## Deployment
* Jeremy Latt, creator, <https://github.com/jlatt>
* Edmund Huber, maintainer, <https://github.com/edmund-huber>
* Niels Freier, added WebSocket support, <https://github.com/stumpyfr>
* apologies to anyone I forgot.
To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*):
```#!bash
$ eris
```
Or you can deploy with [Docker](https://www.docker.com) using the prebuilt [prologic/eris](https://hub.docker.com/r/prologic/eris/):
```#!bash
docker run -d -p 6667:6667 -p 6697:6697 prologic/eris
```
You may want to customize the configuration however and create your own image based off of this; or deploy with `docker stack deploy` on a [Docker Swarm](https://docs.docker.com/engine/swarm/) clsuter like this:
```#!bash
$ docker stack deploy -c docker-compose.yml eris
```
Which assumes a `ircd.yml` coniguration file in the current directory which Docker will use to distribute as the configuration. The `docker-compose.yml` (*Docker Stackfile*) is available at the root of this repository.
## Related Projects
There are a number of supported accompanying services that are being developed alongside Eris:
* [Soter](https://github.com/prologic/soter) -- An IRC Bot that persists channel modes and topics.
* [Cadmus](https://github.com/prologic/cadmus) -- An IRC Bot that logs channels and provides an interface for viewing and searching logs
## Recommended Clients
### CLI / Terminal
* [irccat](https://github.com/prologic/irccat)
* [irssi](https://irssi.org/)
### Cloud
* [IRCCloud](https://www.irccloud.com/)
### Desktop
* [HexChat (Linux)](https://hexchat.github.io/)
* [Textual (OSX)](https://www.codeux.com/textual/)
* [mIRC (Windows)](https://www.mirc.com/)
### Mobile
* [Palaver (iOS)](https://palaverapp.com/) -- SASL, TLS, Server Password, Push Notifications, IRCv3 (*Also supports custom image upload service(s) for better privacy of shared photos/images over IRC*)
### Web
* [Dispatch](https://github.com/khlieng/dispatch) -- TLS, Multiple Servers and Users, Client Certificates
## Related Projects
* [cadmus](https://github.com/prologic/cadmus) -- an IRC Bot written in Go that logs IRC Channels and provides an interface to view and search those logs
* [soter](https://github.com/prologic/soter) -- an IRC Bot written in Go that protects IRC Channels by persisting channel modes and topics
## License
eris is licensed under the MIT License.

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-architect

View File

@@ -1,18 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIC+TCCAeGgAwIBAgIQf965BX8kslTRGmssCj1PlDANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTEwNDIxNTQyMloXDTE4MTEwNDIxNTQy
MlowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKsF2aoSh7Ufb0YCJG+tvJCGZhPecK+OiVbF/wi7eX66aJIj1eLm9v3V
0068lNNZDql0ngTb0dI1IoOscD2YIVdoFidtBOfT8vPjtTzLh7G4XquVML8tG69i
7VutHvQtN/6kLFv+mZB/bHZuQsa1qvTqVjbQnJ5p/UeGd0dN9bYgaNdXsu0C3YbU
683XhpkdwU8tdLlOPNRV3yRZO8L47buhRFYMhQ8y0U9bKHi3akEFzRQphIYoM8kj
RyvvLIfOKFyVj/WUkkpsQWSATuDBEzp84UG+jOoucRr2ck9bMGBYohyvu6ezd8IR
JcjAiQ1pabSetldeTCuavXSnjr8grtkCAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWg
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ
bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQByPoh1182g8+jRd4mqDk6n0/Zx
PSBrhAQd8IID/P3jj8mn8Q8rgktmnEGYSvRVCpsyswiZHW45xvAHjV1msc6tIHSL
Lpkgf1E8qrQ6Cam72/W/dhiKSrVs50K2SGEHY8Ij8d0Q/KPkQqdAC7hbHxD4vbaA
iUAqEZ6d2s9sxEnZvYlol6ITDcQ8kOozSJN51YU5GDT8Vc/qkqRJ3OOn4vss22V2
upXZf33Ci6Dm4xUBd4F0x3dri+OglyLnjWz2sn+OSb/+apS08GiycBqPeYW+KjdA
YHPBnRvjQ35M5FL9cM9tQhcwQ9dPIUy80qehUQjiNOBgh6SD8EGBemLrDEPa
-----END CERTIFICATE-----

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: "3.3"
services:
eris:
image: prologic/eris
configs:
- source: ircd_yml
target: /ircd.yml
- source: ircd_motd
target: /ircd.motd
- source: cert_pem
target: /cert.pem
- source: key_pem
target: /key.pem
ports:
- target: 6667
published: 6667
protocol: tcp
mode: host
- target: 6697
published: 6697
protocol: tcp
mode: host
deploy:
endpoint_mode: dnsrr
restart_policy:
condition: on-failure
replicas: 1
configs:
ircd_yml:
file: ./ircd.yml
ircd_motd:
file: ./ircd.motd
cert_pem:
file: ./cert.pem
key_pem:
file: ./key.pem

19
go.mod Normal file
View File

@@ -0,0 +1,19 @@
module github.com/prologic/eris
go 1.14
require (
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287
github.com/google/uuid v1.1.0 // indirect
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
github.com/imdario/mergo v0.3.11
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3
github.com/prometheus/client_golang v0.9.4
github.com/renstrom/shortuuid v3.0.0+incompatible
github.com/sirupsen/logrus v1.7.0
github.com/stretchr/testify v1.6.0
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/text v0.3.4
gopkg.in/yaml.v2 v2.3.0
)

100
go.sum Normal file
View File

@@ -0,0 +1,100 @@
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287 h1:xOE8jDDulcwdPG+coLps6seNn6yERt5xgKSATNqWUM0=
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287/go.mod h1:nn+Gr++RLey8iGwfvI84UO5oZal6Muz7qPxDII0BsQ8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 h1:KmRLPRstEJiE/9OjumKqI8Rccip8Qmyw2FwyTFxtVqs=
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940/go.mod h1:VOmrX6cmj7zwUeexC9HzznUdTIObHqIXUrWNYS+Ik7w=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3 h1:2YMbJ6WbdQI9K73chxh9OWMDsZ2PNjAIRGTonp3T0l0=
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.4 h1:Y8E/JaaPbmFSW2V81Ab/d8yZFYQQGbni1b1jPcG9Y6A=
github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/renstrom/shortuuid v3.0.0+incompatible h1:F6T1U7bWlI3FTV+JE8HyeR7bkTeYZJntqQLA9ST4HOQ=
github.com/renstrom/shortuuid v3.0.0+incompatible/go.mod h1:n18Ycpn8DijG+h/lLBQVnGKv1BCtTeXo8KKSbBOrQ8c=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3 h1:389FrrKIAlxqQMTscCQ7VH3JAVuxb/pe53v2LBiA7z8=
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3/go.mod h1:QYOctLs5qEsaIrA/PKEc4YqAv2SozbxNEX0vMPs84p4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ const (
var (
SupportedCapabilities = CapabilitySet{
MultiPrefix: true,
SASL: true,
}
)

View File

@@ -5,10 +5,10 @@ import (
)
type Channel struct {
flags ChannelModeSet
flags *ChannelModeSet
lists map[ChannelMode]*UserMaskSet
key Text
members MemberSet
members *MemberSet
name Name
server *Server
topic Text
@@ -17,26 +17,32 @@ type Channel struct {
// NewChannel creates a new channel from a `Server` and a `name`
// string, which must be unique on the server.
func NewChannel(s *Server, name Name) *Channel {
func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
channel := &Channel{
flags: make(ChannelModeSet),
flags: NewChannelModeSet(),
lists: map[ChannelMode]*UserMaskSet{
BanMask: NewUserMaskSet(),
ExceptMask: NewUserMaskSet(),
InviteMask: NewUserMaskSet(),
},
members: make(MemberSet),
members: NewMemberSet(),
name: name,
server: s,
}
if addDefaultModes {
for _, mode := range DefaultChannelModes {
channel.flags.Set(mode)
}
}
s.channels.Add(channel)
return channel
}
func (channel *Channel) IsEmpty() bool {
return len(channel.members) == 0
return channel.members.Count() == 0
}
func (channel *Channel) Names(client *Client) {
@@ -45,31 +51,34 @@ func (channel *Channel) Names(client *Client) {
}
func (channel *Channel) ClientIsOperator(client *Client) bool {
return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator)
return client.modes.Has(Operator) || channel.members.HasMode(client, ChannelOperator)
}
func (channel *Channel) Nicks(target *Client) []string {
isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix]
nicks := make([]string, len(channel.members))
channel.members.RLock()
defer channel.members.RUnlock()
nicks := make([]string, channel.members.Count())
i := 0
for client, modes := range channel.members {
channel.members.Range(func(client *Client, modes *ChannelModeSet) bool {
if isMultiPrefix {
if modes[ChannelOperator] {
if modes.Has(ChannelOperator) {
nicks[i] += "@"
}
if modes[Voice] {
if modes.Has(Voice) {
nicks[i] += "+"
}
} else {
if modes[ChannelOperator] {
if modes.Has(ChannelOperator) {
nicks[i] += "@"
} else if modes[Voice] {
} else if modes.Has(Voice) {
nicks[i] += "+"
}
}
nicks[i] += client.Nick().String()
i += 1
}
i++
return true
})
return nicks
}
@@ -87,7 +96,7 @@ func (channel *Channel) String() string {
// <mode> <mode params>
func (channel *Channel) ModeString(client *Client) (str string) {
isMember := client.flags[Operator] || channel.members.Has(client)
isMember := client.modes.Has(Operator) || channel.members.Has(client)
showKey := isMember && (channel.key != "")
showUserLimit := channel.userLimit > 0
@@ -100,9 +109,10 @@ func (channel *Channel) ModeString(client *Client) (str string) {
}
// flags
for mode := range channel.flags {
channel.flags.Range(func(mode ChannelMode) bool {
str += mode.String()
}
return true
})
str = "+" + str
@@ -120,7 +130,7 @@ func (channel *Channel) ModeString(client *Client) (str string) {
func (channel *Channel) IsFull() bool {
return (channel.userLimit > 0) &&
(uint64(len(channel.members)) >= channel.userLimit)
(uint64(channel.members.Count()) >= channel.userLimit)
}
func (channel *Channel) CheckKey(key Text) bool {
@@ -145,31 +155,32 @@ func (channel *Channel) Join(client *Client, key Text) {
return
}
isInvited := channel.lists[InviteMask].Match(client.UserHost())
if !isOperator && channel.flags[InviteOnly] && !isInvited {
isInvited := channel.lists[InviteMask].Match(client.UserHost(false))
if !isOperator && channel.flags.Has(InviteOnly) && !isInvited {
client.ErrInviteOnlyChan(channel)
return
}
if channel.lists[BanMask].Match(client.UserHost()) &&
if channel.lists[BanMask].Match(client.UserHost(false)) &&
!isInvited &&
!isOperator &&
!channel.lists[ExceptMask].Match(client.UserHost()) {
!channel.lists[ExceptMask].Match(client.UserHost(false)) {
client.ErrBannedFromChan(channel)
return
}
client.channels.Add(channel)
channel.members.Add(client)
if len(channel.members) == 1 {
channel.members[client][ChannelCreator] = true
channel.members[client][ChannelOperator] = true
if channel.members.Count() == 1 {
channel.members.Get(client).Set(ChannelCreator)
channel.members.Get(client).Set(ChannelOperator)
}
reply := RplJoin(client, channel)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
member.Reply(reply)
}
return true
})
channel.GetTopic(client)
channel.Names(client)
}
@@ -181,9 +192,10 @@ func (channel *Channel) Part(client *Client, message Text) {
}
reply := RplPart(client, channel, message)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
member.Reply(reply)
}
return true
})
channel.Quit(client)
}
@@ -194,8 +206,7 @@ func (channel *Channel) GetTopic(client *Client) {
}
if channel.topic == "" {
// clients appear not to expect this
//replier.Reply(RplNoTopic(channel))
client.RplNoTopic(channel)
return
}
@@ -208,7 +219,7 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
return
}
if channel.flags[OpOnlyTopic] && !channel.ClientIsOperator(client) {
if channel.flags.Has(OpOnlyTopic) && !channel.ClientIsOperator(client) {
client.ErrChanOPrivIsNeeded(channel)
return
}
@@ -216,22 +227,26 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
channel.topic = topic
reply := RplTopicMsg(client, channel)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
member.Reply(reply)
}
return true
})
}
func (channel *Channel) CanSpeak(client *Client) bool {
if channel.ClientIsOperator(client) {
return true
}
if channel.flags[NoOutside] && !channel.members.Has(client) {
if channel.flags.Has(NoOutside) && !channel.members.Has(client) {
return false
}
if channel.flags[Moderated] && !(channel.members.HasMode(client, Voice) ||
if channel.flags.Has(Moderated) && !(channel.members.HasMode(client, Voice) ||
channel.members.HasMode(client, ChannelOperator)) {
return false
}
if channel.flags.Has(SecureChan) && !client.modes.Has(SecureConn) {
return false
}
return true
}
@@ -241,12 +256,14 @@ func (channel *Channel) PrivMsg(client *Client, message Text) {
return
}
reply := RplPrivMsg(client, channel, message)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
if member == client {
continue
return true
}
client.server.metrics.Counter("client", "messages").Inc()
member.Reply(reply)
}
return true
})
}
func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
@@ -258,17 +275,17 @@ func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
switch op {
case Add:
if channel.flags[mode] {
if channel.flags.Has(mode) {
return false
}
channel.flags[mode] = true
channel.flags.Set(mode)
return true
case Remove:
if !channel.flags[mode] {
if !channel.flags.Has(mode) {
return false
}
delete(channel.flags, mode)
channel.flags.Unset(mode)
return true
}
return false
@@ -299,17 +316,17 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode,
switch op {
case Add:
if channel.members[target][mode] {
if channel.members.Get(target).Has(mode) {
return false
}
channel.members[target][mode] = true
channel.members.Get(target).Set(mode)
return true
case Remove:
if !channel.members[target][mode] {
if !channel.members.Get(target).Has(mode) {
return false
}
channel.members[target][mode] = false
channel.members.Get(target).Unset(mode)
return true
}
return false
@@ -357,7 +374,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo
return channel.applyModeMask(client, change.mode, change.op,
NewName(change.arg))
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private:
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private, Secret, SecureChan:
return channel.applyModeFlag(client, change.mode, change.op)
case Key:
@@ -423,9 +440,10 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) {
if len(applied) > 0 {
reply := RplChannelMode(client, channel, applied)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
member.Reply(reply)
}
return true
})
}
}
@@ -435,17 +453,21 @@ func (channel *Channel) Notice(client *Client, message Text) {
return
}
reply := RplNotice(client, channel, message)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
if member == client {
continue
return true
}
client.server.metrics.Counter("client", "messages").Inc()
member.Reply(reply)
}
return true
})
}
func (channel *Channel) Quit(client *Client) {
channel.members.Remove(client)
client.channels.Remove(channel)
// XXX: Race Condition from client.destroy()
// Do we need to?
// client.channels.Remove(channel)
if channel.IsEmpty() {
channel.server.channels.Remove(channel)
@@ -467,14 +489,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment Text) {
}
reply := RplKick(channel, client, target, comment)
for member := range channel.members {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
member.Reply(reply)
}
return true
})
channel.Quit(target)
}
func (channel *Channel) Invite(invitee *Client, inviter *Client) {
if channel.flags[InviteOnly] && !channel.ClientIsOperator(inviter) {
if channel.flags.Has(InviteOnly) && !channel.ClientIsOperator(inviter) {
inviter.ErrChanOPrivIsNeeded(channel)
return
}
@@ -484,13 +507,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) {
return
}
if channel.flags[InviteOnly] {
channel.lists[InviteMask].Add(invitee.UserHost())
if channel.flags.Has(InviteOnly) {
channel.lists[InviteMask].Add(invitee.UserHost(false))
}
inviter.RplInviting(invitee, channel.name)
invitee.Reply(RplInviteMsg(inviter, invitee, channel.name))
if invitee.flags[Away] {
if invitee.modes.Has(Away) {
inviter.RplAway(invitee)
}
}

View File

@@ -4,7 +4,10 @@ import (
"crypto/tls"
"fmt"
"net"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
const (
@@ -12,75 +15,118 @@ const (
QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked
)
type SyncBool struct {
sync.RWMutex
value bool
}
func NewSyncBool(value bool) *SyncBool {
return &SyncBool{value: value}
}
func (sb *SyncBool) Get() bool {
sb.RLock()
defer sb.RUnlock()
return sb.value
}
func (sb *SyncBool) Set(value bool) {
sb.Lock()
defer sb.Unlock()
sb.value = value
}
type Client struct {
atime time.Time
authorized bool
awayMessage Text
capabilities CapabilitySet
capState CapState
channels ChannelSet
channels *ChannelSet
ctime time.Time
flags map[UserMode]bool
hasQuit bool
modes *UserModeSet
hasQuit *SyncBool
hops uint
hostname Name
hostmask Name // Cloacked hostname (SHA256)
pingTime time.Time
idleTimer *time.Timer
nick Name
quitTimer *time.Timer
realname Text
registered bool
sasl *SaslState
server *Server
socket *Socket
replies chan string
username Name
}
func NewClient(server *Server, conn net.Conn) *Client {
now := time.Now()
client := &Client{
c := &Client{
atime: now,
authorized: len(server.password) == 0,
capState: CapNone,
capabilities: make(CapabilitySet),
channels: make(ChannelSet),
channels: NewChannelSet(),
ctime: now,
flags: make(map[UserMode]bool),
modes: NewUserModeSet(),
hasQuit: NewSyncBool(false),
sasl: NewSaslState(),
server: server,
socket: NewSocket(conn),
replies: make(chan string),
}
if _, ok := conn.(*tls.Conn); ok {
client.flags[SecureConn] = true
client.flags[SecureOnly] = true
c.modes.Set(SecureConn)
}
client.Touch()
go client.run()
c.Touch()
go c.writeloop()
go c.readloop()
return client
return c
}
//
// command goroutine
//
func (client *Client) run() {
func (c *Client) writeloop() {
for {
select {
case reply, ok := <-c.replies:
if !ok || reply == "" || c.socket == nil {
return
}
c.socket.Write(reply)
}
}
}
func (c *Client) readloop() {
var command Command
var err error
var line string
// Set the hostname for this client.
client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr())
c.hostname = AddrLookupHostname(c.socket.conn.RemoteAddr())
c.hostmask = NewName(SHA256(c.hostname.String()))
for err == nil {
if line, err = client.socket.Read(); err != nil {
if line, err = c.socket.Read(); err != nil {
command = NewQuitCommand("connection closed")
} else if command, err = ParseCommand(line); err != nil {
switch err {
case ErrParseCommand:
//TODO(dan): use the real failed numeric for this (400)
client.Reply(RplNotice(client.server, client,
NewText("failed to parse command")))
c.Reply(RplNotice(c.server, c, NewText("failed to parse command")))
case NotEnoughArgsError:
// TODO
@@ -90,7 +136,7 @@ func (client *Client) run() {
continue
} else if checkPass, ok := command.(checkPasswordCommand); ok {
checkPass.LoadPassword(client.server)
checkPass.LoadPassword(c.server)
// Block the client thread while handling a potentially expensive
// password bcrypt operation. Since the server is single-threaded
// for commands, we don't want the server to perform the bcrypt,
@@ -99,138 +145,190 @@ func (client *Client) run() {
checkPass.CheckPassword()
}
client.send(command)
c.processCommand(command)
}
}
func (client *Client) send(command Command) {
command.SetClient(client)
client.server.commands <- command
func (c *Client) processCommand(cmd Command) {
cmd.SetClient(c)
if !c.registered {
regCmd, ok := cmd.(RegServerCommand)
if !ok {
c.Quit("unexpected command")
return
}
regCmd.HandleRegServer(c.server)
return
}
srvCmd, ok := cmd.(ServerCommand)
if !ok {
c.ErrUnknownCommand(cmd.Code())
return
}
c.server.metrics.Counter("client", "commands").Inc()
defer func(t time.Time) {
v := c.server.metrics.SummaryVec("client", "command_duration_seconds")
v.WithLabelValues(cmd.Code().String()).Observe(time.Now().Sub(t).Seconds())
}(time.Now())
switch srvCmd.(type) {
case *PingCommand, *PongCommand:
c.Touch()
case *QuitCommand:
// no-op
default:
c.Active()
c.Touch()
}
srvCmd.HandleServer(c.server)
}
// quit timer goroutine
func (client *Client) connectionTimeout() {
client.send(NewQuitCommand("connection timeout"))
func (c *Client) connectionTimeout() {
c.processCommand(NewQuitCommand("connection timeout"))
}
//
// idle timer goroutine
//
func (client *Client) connectionIdle() {
client.server.idle <- client
func (c *Client) connectionIdle() {
c.server.idle <- c
}
//
// server goroutine
//
func (client *Client) Active() {
client.atime = time.Now()
func (c *Client) Active() {
c.atime = time.Now()
}
func (client *Client) Touch() {
if client.quitTimer != nil {
client.quitTimer.Stop()
func (c *Client) Touch() {
if c.quitTimer != nil {
c.quitTimer.Stop()
}
if client.idleTimer == nil {
client.idleTimer = time.AfterFunc(IDLE_TIMEOUT, client.connectionIdle)
if c.idleTimer == nil {
c.idleTimer = time.AfterFunc(IDLE_TIMEOUT, c.connectionIdle)
} else {
client.idleTimer.Reset(IDLE_TIMEOUT)
c.idleTimer.Reset(IDLE_TIMEOUT)
}
}
func (client *Client) Idle() {
client.Reply(RplPing(client.server))
func (c *Client) Idle() {
c.pingTime = time.Now()
c.Reply(RplPing(c.server))
if client.quitTimer == nil {
client.quitTimer = time.AfterFunc(QUIT_TIMEOUT, client.connectionTimeout)
if c.quitTimer == nil {
c.quitTimer = time.AfterFunc(QUIT_TIMEOUT, c.connectionTimeout)
} else {
client.quitTimer.Reset(QUIT_TIMEOUT)
c.quitTimer.Reset(QUIT_TIMEOUT)
}
}
func (client *Client) Register() {
if client.registered {
func (c *Client) Register() {
if c.registered {
return
}
client.registered = true
client.Touch()
c.registered = true
c.modes.Set(HostMask)
c.Touch()
}
func (client *Client) destroy() {
func (c *Client) destroy() {
// clean up channels
for channel := range client.channels {
channel.Quit(client)
}
c.channels.Range(func(channel *Channel) bool {
channel.Quit(c)
return true
})
// clean up server
client.server.clients.Remove(client)
if _, ok := c.socket.conn.(*tls.Conn); ok {
c.server.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Dec()
} else {
c.server.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Dec()
}
c.server.connections.Dec()
c.server.clients.Remove(c)
// clean up self
if client.idleTimer != nil {
client.idleTimer.Stop()
if c.idleTimer != nil {
c.idleTimer.Stop()
}
if client.quitTimer != nil {
client.quitTimer.Stop()
if c.quitTimer != nil {
c.quitTimer.Stop()
}
client.socket.Close()
close(c.replies)
Log.debug.Printf("%s: destroyed", client)
c.socket.Close()
log.Debugf("%s: destroyed", c)
}
func (client *Client) IdleTime() time.Duration {
return time.Since(client.atime)
func (c *Client) IdleTime() time.Duration {
return time.Since(c.atime)
}
func (client *Client) SignonTime() int64 {
return client.ctime.Unix()
func (c *Client) SignonTime() int64 {
return c.ctime.Unix()
}
func (client *Client) IdleSeconds() uint64 {
return uint64(client.IdleTime().Seconds())
func (c *Client) IdleSeconds() uint64 {
return uint64(c.IdleTime().Seconds())
}
func (client *Client) HasNick() bool {
return client.nick != ""
func (c *Client) HasNick() bool {
return c.nick != ""
}
func (client *Client) HasUsername() bool {
return client.username != ""
func (c *Client) HasUsername() bool {
return c.username != ""
}
func (client *Client) CanSpeak(target *Client) bool {
requiresSecure := client.flags[SecureOnly] || target.flags[SecureOnly]
isSecure := client.flags[SecureConn] && target.flags[SecureConn]
isOperator := client.flags[Operator]
func (c *Client) CanSpeak(target *Client) bool {
requiresSecure := c.modes.Has(SecureOnly) || target.modes.Has(SecureOnly)
isSecure := c.modes.Has(SecureConn) && target.modes.Has(SecureConn)
isOperator := c.modes.Has(Operator)
return requiresSecure && (isOperator || isSecure)
return !requiresSecure || (requiresSecure && (isOperator || isSecure))
}
// <mode>
func (c *Client) ModeString() (str string) {
for flag := range c.flags {
str += flag.String()
}
if len(str) > 0 {
str = "+" + str
}
return
return c.modes.String()
}
func (c *Client) UserHost() Name {
func (c *Client) UserHost(cloacked bool) Name {
username := "*"
if c.HasUsername() {
if c.username != "" {
username = c.username.String()
}
return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname))
if cloacked {
return Name(fmt.Sprintf("%s!%s@%s", c.nick, username, c.hostmask))
}
return Name(fmt.Sprintf("%s!%s@%s", c.nick, username, c.hostname))
}
func (c *Client) Server() Name {
return c.server.name
}
func (c *Client) ServerInfo() string {
return c.server.description
}
func (c *Client) Nick() Name {
@@ -241,65 +339,71 @@ func (c *Client) Nick() Name {
}
func (c *Client) Id() Name {
return c.UserHost()
return c.UserHost(true)
}
func (c *Client) String() string {
return c.Id().String()
}
func (client *Client) Friends() ClientSet {
friends := make(ClientSet)
friends.Add(client)
for channel := range client.channels {
for member := range channel.members {
func (c *Client) Friends() *ClientSet {
friends := NewClientSet()
friends.Add(c)
c.channels.Range(func(channel *Channel) bool {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
friends.Add(member)
}
}
return true
})
return true
})
return friends
}
func (client *Client) SetNickname(nickname Name) {
if client.HasNick() {
Log.error.Printf("%s nickname already set!", client)
func (c *Client) SetNickname(nickname Name) {
if c.nick != "" {
log.Errorf("%s nickname already set!", c)
return
}
client.nick = nickname
client.server.clients.Add(client)
c.nick = nickname
c.server.clients.Add(c)
}
func (client *Client) ChangeNickname(nickname Name) {
func (c *Client) ChangeNickname(nickname Name) {
// Make reply before changing nick to capture original source id.
reply := RplNick(client, nickname)
client.server.clients.Remove(client)
client.server.whoWas.Append(client)
client.nick = nickname
client.server.clients.Add(client)
for friend := range client.Friends() {
reply := RplNick(c, nickname)
c.server.clients.Remove(c)
c.server.whoWas.Append(c)
c.nick = nickname
c.server.clients.Add(c)
c.Friends().Range(func(friend *Client) bool {
friend.Reply(reply)
return true
})
}
func (c *Client) Reply(reply string) {
if !c.hasQuit.Get() {
c.replies <- reply
}
}
func (client *Client) Reply(reply string) error {
return client.socket.Write(reply)
}
func (client *Client) Quit(message Text) {
if client.hasQuit {
func (c *Client) Quit(message Text) {
if c.hasQuit.Get() {
return
}
client.hasQuit = true
client.Reply(RplError("quit"))
client.server.whoWas.Append(client)
friends := client.Friends()
friends.Remove(client)
client.destroy()
c.hasQuit.Set(true)
c.Reply(RplError("quit"))
c.server.whoWas.Append(c)
friends := c.Friends()
friends.Remove(c)
c.destroy()
if len(friends) > 0 {
reply := RplQuit(client, message)
for friend := range friends {
if friends.Count() > 0 {
reply := RplQuit(c, message)
friends.Range(func(friend *Client) bool {
friend.Reply(reply)
}
return true
})
}
}

View File

@@ -1,30 +1,20 @@
package irc
import (
"database/sql"
"errors"
"log"
"regexp"
"strings"
"sync"
"github.com/DanielOaks/girc-go/ircmatch"
)
var (
ErrNickMissing = errors.New("nick missing")
ErrNicknameInUse = errors.New("nickname in use")
ErrNicknameMismatch = errors.New("nickname mismatch")
wildMaskExpr = regexp.MustCompile(`\*|\?`)
likeQuoter = strings.NewReplacer(
`\`, `\\`,
`%`, `\%`,
`_`, `\_`,
`*`, `%`,
`?`, `_`)
)
func HasWildcards(mask string) bool {
return wildMaskExpr.MatchString(mask)
}
func ExpandUserHost(userhost Name) (expanded Name) {
expanded = userhost
// fill in missing wildcards for nicks
@@ -37,24 +27,29 @@ func ExpandUserHost(userhost Name) (expanded Name) {
return
}
func QuoteLike(userhost Name) string {
return likeQuoter.Replace(userhost.String())
}
type ClientLookupSet struct {
byNick map[Name]*Client
db *ClientDB
sync.RWMutex
nicks map[Name]*Client
}
func NewClientLookupSet() *ClientLookupSet {
return &ClientLookupSet{
byNick: make(map[Name]*Client),
db: NewClientDB(),
nicks: make(map[Name]*Client),
}
}
func (clients *ClientLookupSet) Count() int {
clients.RLock()
defer clients.RUnlock()
return len(clients.nicks)
}
func (clients *ClientLookupSet) Get(nick Name) *Client {
return clients.byNick[nick.ToLower()]
clients.RLock()
defer clients.RUnlock()
return clients.nicks[nick.ToLower()]
}
func (clients *ClientLookupSet) Add(client *Client) error {
@@ -64,8 +59,11 @@ func (clients *ClientLookupSet) Add(client *Client) error {
if clients.Get(client.nick) != nil {
return ErrNicknameInUse
}
clients.byNick[client.Nick().ToLower()] = client
clients.db.Add(client)
clients.Lock()
defer clients.Unlock()
clients.nicks[client.Nick().ToLower()] = client
return nil
}
@@ -76,96 +74,60 @@ func (clients *ClientLookupSet) Remove(client *Client) error {
if clients.Get(client.nick) != client {
return ErrNicknameMismatch
}
delete(clients.byNick, client.nick.ToLower())
clients.db.Remove(client)
clients.Lock()
defer clients.Unlock()
delete(clients.nicks, client.nick.ToLower())
return nil
}
func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) {
userhost = ExpandUserHost(userhost)
set = make(ClientSet)
rows, err := clients.db.db.Query(
`SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`,
QuoteLike(userhost))
if err != nil {
Log.error.Println("ClientLookupSet.FindAll.Query:", err)
return
}
for rows.Next() {
var sqlNickname string
err := rows.Scan(&sqlNickname)
if err != nil {
Log.error.Println("ClientLookupSet.FindAll.Scan:", err)
func (clients *ClientLookupSet) Range(f func(nick Name, client *Client) bool) {
clients.RLock()
defer clients.RUnlock()
for nick, client := range clients.nicks {
if !f(nick, client) {
return
}
nickname := Name(sqlNickname)
client := clients.Get(nickname)
if client == nil {
Log.error.Println("ClientLookupSet.FindAll: missing client:", nickname)
continue
}
set.Add(client)
}
return
}
func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
clients.RLock()
defer clients.RUnlock()
set := NewClientSet()
userhost = ExpandUserHost(userhost)
matcher := ircmatch.MakeMatch(userhost.String())
var casemappedNickMask string
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
set.Add(client)
}
}
return set
}
func (clients *ClientLookupSet) Find(userhost Name) *Client {
clients.RLock()
defer clients.RUnlock()
userhost = ExpandUserHost(userhost)
row := clients.db.db.QueryRow(
`SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\' LIMIT 1`,
QuoteLike(userhost))
var nickname Name
err := row.Scan(&nickname)
if err != nil {
Log.error.Println("ClientLookupSet.Find:", err)
return nil
}
return clients.Get(nickname)
}
matcher := ircmatch.MakeMatch(userhost.String())
//
// client db
//
type ClientDB struct {
db *sql.DB
}
func NewClientDB() *ClientDB {
db := &ClientDB{
db: OpenDB(":memory:"),
}
stmts := []string{
`CREATE TABLE client (
nickname TEXT NOT NULL COLLATE NOCASE UNIQUE,
userhost TEXT NOT NULL COLLATE NOCASE,
UNIQUE (nickname, userhost) ON CONFLICT REPLACE)`,
`CREATE UNIQUE INDEX idx_nick ON client (nickname COLLATE NOCASE)`,
`CREATE UNIQUE INDEX idx_uh ON client (userhost COLLATE NOCASE)`,
}
for _, stmt := range stmts {
_, err := db.db.Exec(stmt)
if err != nil {
log.Fatal("NewClientDB: ", stmt, err)
var casemappedNickMask string
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
return client
}
}
return db
}
func (db *ClientDB) Add(client *Client) {
_, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`,
client.Nick().String(), client.UserHost().String())
if err != nil {
Log.error.Println("ClientDB.Add:", err)
}
}
func (db *ClientDB) Remove(client *Client) {
_, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`,
client.Nick().String())
if err != nil {
Log.error.Println("ClientDB.Remove:", err)
}
return nil
}
//

View File

@@ -26,34 +26,38 @@ var (
NotEnoughArgsError = errors.New("not enough arguments")
ErrParseCommand = errors.New("failed to parse message")
parseCommandFuncs = map[StringCode]parseCommandFunc{
AWAY: ParseAwayCommand,
CAP: ParseCapCommand,
INVITE: ParseInviteCommand,
ISON: ParseIsOnCommand,
JOIN: ParseJoinCommand,
KICK: ParseKickCommand,
KILL: ParseKillCommand,
LIST: ParseListCommand,
MODE: ParseModeCommand,
MOTD: ParseMOTDCommand,
NAMES: ParseNamesCommand,
NICK: ParseNickCommand,
NOTICE: ParseNoticeCommand,
ONICK: ParseOperNickCommand,
OPER: ParseOperCommand,
PART: ParsePartCommand,
PASS: ParsePassCommand,
PING: ParsePingCommand,
PONG: ParsePongCommand,
PRIVMSG: ParsePrivMsgCommand,
QUIT: ParseQuitCommand,
TIME: ParseTimeCommand,
TOPIC: ParseTopicCommand,
USER: ParseUserCommand,
VERSION: ParseVersionCommand,
WHO: ParseWhoCommand,
WHOIS: ParseWhoisCommand,
WHOWAS: ParseWhoWasCommand,
AUTHENTICATE: ParseAuthenticateCommand,
AWAY: ParseAwayCommand,
CAP: ParseCapCommand,
INVITE: ParseInviteCommand,
ISON: ParseIsOnCommand,
JOIN: ParseJoinCommand,
KICK: ParseKickCommand,
KILL: ParseKillCommand,
LIST: ParseListCommand,
MODE: ParseModeCommand,
MOTD: ParseMOTDCommand,
NAMES: ParseNamesCommand,
NICK: ParseNickCommand,
NOTICE: ParseNoticeCommand,
ONICK: ParseOperNickCommand,
OPER: ParseOperCommand,
REHASH: ParseRehashCommand,
PART: ParsePartCommand,
PASS: ParsePassCommand,
PING: ParsePingCommand,
PONG: ParsePongCommand,
PRIVMSG: ParsePrivMsgCommand,
QUIT: ParseQuitCommand,
TIME: ParseTimeCommand,
LUSERS: ParseLUsersCommand,
TOPIC: ParseTopicCommand,
USER: ParseUserCommand,
VERSION: ParseVersionCommand,
WALLOPS: ParseWallopsCommand,
WHO: ParseWhoCommand,
WHOIS: ParseWhoisCommand,
WHOWAS: ParseWhoWasCommand,
}
)
@@ -180,6 +184,22 @@ func ParsePongCommand(args []string) (Command, error) {
return message, nil
}
// AUTHENTICATE <arg>
type AuthenticateCommand struct {
BaseCommand
arg string
}
func ParseAuthenticateCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
return &AuthenticateCommand{
arg: args[0],
}, nil
}
// PASS <password>
type PassCommand struct {
@@ -641,6 +661,15 @@ func ParseOperCommand(args []string) (Command, error) {
return cmd, nil
}
type RehashCommand struct {
BaseCommand
}
// REHASH
func ParseRehashCommand(args []string) (Command, error) {
return &RehashCommand{}, nil
}
type CapCommand struct {
BaseCommand
subCommand CapSubCommand
@@ -840,6 +869,14 @@ func ParseTimeCommand(args []string) (Command, error) {
return cmd, nil
}
type LUsersCommand struct {
BaseCommand
}
func ParseLUsersCommand(args []string) (Command, error) {
return &LUsersCommand{}, nil
}
type KillCommand struct {
BaseCommand
nickname Name
@@ -856,6 +893,20 @@ func ParseKillCommand(args []string) (Command, error) {
}, nil
}
type WallopsCommand struct {
BaseCommand
message Text
}
func ParseWallopsCommand(args []string) (Command, error) {
if len(args) < 1 {
return nil, NotEnoughArgsError
}
return &WallopsCommand{
message: NewText(args[0]),
}, nil
}
type WhoWasCommand struct {
BaseCommand
nicknames []Name

View File

@@ -4,7 +4,9 @@ import (
"errors"
"io/ioutil"
"log"
"sync"
"github.com/imdario/mergo"
"gopkg.in/yaml.v2"
)
@@ -26,17 +28,25 @@ func (conf *PassConfig) PasswordBytes() []byte {
}
type Config struct {
sync.Mutex
filename string
Network struct {
Name string
}
Server struct {
PassConfig `yaml:",inline"`
Database string
Listen []string
TLSListen map[string]*TLSConfig
Log string
MOTD string
Name string
PassConfig `yaml:",inline"`
Listen []string
TLSListen map[string]*TLSConfig
Log string
MOTD string
Name string
Description string
}
Operator map[string]*PassConfig
Account map[string]*PassConfig
}
func (conf *Config) Operators() map[Name][]byte {
@@ -47,6 +57,35 @@ func (conf *Config) Operators() map[Name][]byte {
return operators
}
func (conf *Config) Accounts() map[string][]byte {
accounts := make(map[string][]byte)
for name, account := range conf.Account {
accounts[name] = []byte(account.Password)
}
return accounts
}
func (conf *Config) Name() string {
return conf.filename
}
func (conf *Config) Reload() error {
conf.Lock()
defer conf.Unlock()
newconf, err := LoadConfig(conf.filename)
if err != nil {
return nil
}
err = mergo.MergeWithOverwrite(conf, newconf)
if err != nil {
return nil
}
return nil
}
func LoadConfig(filename string) (config *Config, err error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
@@ -58,11 +97,23 @@ func LoadConfig(filename string) (config *Config, err error) {
return nil, err
}
config.filename = filename
if config.Network.Name == "" {
return nil, errors.New("Network name missing")
}
if config.Server.Name == "" {
return nil, errors.New("Server name missing")
}
if !IsHostname(config.Server.Name) {
return nil, errors.New("Server name must match the format of a hostname")
}
if len(config.Server.Listen)+len(config.Server.TLSListen) == 0 {
return nil, errors.New("Server listening addresses missing")
}
return config, nil
}

View File

@@ -5,36 +5,39 @@ const (
MAX_REPLY_LEN = 512 - len(CRLF)
// string codes
AWAY StringCode = "AWAY"
CAP StringCode = "CAP"
DEBUG StringCode = "DEBUG"
ERROR StringCode = "ERROR"
INVITE StringCode = "INVITE"
ISON StringCode = "ISON"
JOIN StringCode = "JOIN"
KICK StringCode = "KICK"
KILL StringCode = "KILL"
LIST StringCode = "LIST"
MODE StringCode = "MODE"
MOTD StringCode = "MOTD"
NAMES StringCode = "NAMES"
NICK StringCode = "NICK"
NOTICE StringCode = "NOTICE"
ONICK StringCode = "ONICK"
OPER StringCode = "OPER"
PART StringCode = "PART"
PASS StringCode = "PASS"
PING StringCode = "PING"
PONG StringCode = "PONG"
PRIVMSG StringCode = "PRIVMSG"
QUIT StringCode = "QUIT"
TIME StringCode = "TIME"
TOPIC StringCode = "TOPIC"
USER StringCode = "USER"
VERSION StringCode = "VERSION"
WHO StringCode = "WHO"
WHOIS StringCode = "WHOIS"
WHOWAS StringCode = "WHOWAS"
AUTHENTICATE StringCode = "AUTHENTICATE" // SASL
AWAY StringCode = "AWAY"
CAP StringCode = "CAP"
ERROR StringCode = "ERROR"
INVITE StringCode = "INVITE"
ISON StringCode = "ISON"
JOIN StringCode = "JOIN"
KICK StringCode = "KICK"
KILL StringCode = "KILL"
LIST StringCode = "LIST"
MODE StringCode = "MODE"
MOTD StringCode = "MOTD"
NAMES StringCode = "NAMES"
NICK StringCode = "NICK"
NOTICE StringCode = "NOTICE"
ONICK StringCode = "ONICK"
OPER StringCode = "OPER"
REHASH StringCode = "REHASH"
PART StringCode = "PART"
PASS StringCode = "PASS"
PING StringCode = "PING"
PONG StringCode = "PONG"
PRIVMSG StringCode = "PRIVMSG"
QUIT StringCode = "QUIT"
TIME StringCode = "TIME"
LUSERS StringCode = "LUSERS"
TOPIC StringCode = "TOPIC"
USER StringCode = "USER"
VERSION StringCode = "VERSION"
WALLOPS StringCode = "WALLOPS"
WHO StringCode = "WHO"
WHOIS StringCode = "WHOIS"
WHOWAS StringCode = "WHOWAS"
// numeric codes
RPL_WELCOME NumericCode = 1
@@ -90,6 +93,7 @@ const (
RPL_LISTEND NumericCode = 323
RPL_CHANNELMODEIS NumericCode = 324
RPL_UNIQOPIS NumericCode = 325
RPL_WHOISLOGGEDIN NumericCode = 330
RPL_NOTOPIC NumericCode = 331
RPL_TOPIC NumericCode = 332
RPL_INVITING NumericCode = 341
@@ -176,4 +180,15 @@ const (
ERR_UMODEUNKNOWNFLAG NumericCode = 501
ERR_USERSDONTMATCH NumericCode = 502
RPL_WHOISSECURE NumericCode = 671
// SASL
RPL_LOGGEDIN NumericCode = 900
RPL_LOGGEDOUT NumericCode = 901
ERR_NICKLOCKED NumericCode = 902
RPL_SASLSUCCESS NumericCode = 903
ERR_SASLFAIL NumericCode = 904
ERR_SASLTOOLONG NumericCode = 905
ERR_SASLABORTED NumericCode = 906
ERR_SASLALREADY NumericCode = 907
RPL_SASLMECHS NumericCode = 908
)

View File

@@ -1,17 +0,0 @@
package irc
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
)
// OpenDB opens a connection to a sqlite3 database given a path
func OpenDB(path string) *sql.DB {
db, err := sql.Open("sqlite3", path)
if err != nil {
log.Fatal("open db error: ", err)
}
return db
}

View File

@@ -1,60 +0,0 @@
package irc
import (
"io"
"log"
"os"
)
type Logging struct {
debug *log.Logger
info *log.Logger
warn *log.Logger
error *log.Logger
}
var (
levels = map[string]uint8{
"debug": 4,
"info": 3,
"warn": 2,
"error": 1,
}
devNull io.Writer
)
func init() {
var err error
devNull, err = os.Open(os.DevNull)
if err != nil {
log.Fatal(err)
}
}
func NewLogger(on bool) *log.Logger {
return log.New(output(on), "", log.LstdFlags)
}
func output(on bool) io.Writer {
if on {
return os.Stdout
}
return devNull
}
func (logging *Logging) SetLevel(level string) {
logging.debug = NewLogger(levels[level] >= levels["debug"])
logging.info = NewLogger(levels[level] >= levels["info"])
logging.warn = NewLogger(levels[level] >= levels["warn"])
logging.error = NewLogger(levels[level] >= levels["error"])
}
func NewLogging(level string) *Logging {
logging := &Logging{}
logging.SetLevel(level)
return logging
}
var (
Log = NewLogging("warn")
)

236
irc/metrics.go Normal file
View File

@@ -0,0 +1,236 @@
package irc
import (
"fmt"
"net/http"
"sync"
log "github.com/sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// DefObjectives ...
var DefObjectives = map[float64]float64{
0.50: 0.05,
0.90: 0.01,
0.95: 0.005,
0.99: 0.001,
}
// Metrics ...
type Metrics struct {
sync.RWMutex
namespace string
metrics map[string]prometheus.Metric
guagevecs map[string]*prometheus.GaugeVec
sumvecs map[string]*prometheus.SummaryVec
}
// NewMetrics ...
func NewMetrics(namespace string) *Metrics {
return &Metrics{
namespace: namespace,
metrics: make(map[string]prometheus.Metric),
guagevecs: make(map[string]*prometheus.GaugeVec),
sumvecs: make(map[string]*prometheus.SummaryVec),
}
}
// NewCounter ...
func (m *Metrics) NewCounter(subsystem, name, help string) prometheus.Counter {
counter := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.metrics[key] = counter
m.Unlock()
prometheus.MustRegister(counter)
return counter
}
// NewCounterFunc ...
func (m *Metrics) NewCounterFunc(subsystem, name, help string, f func() float64) prometheus.CounterFunc {
counter := prometheus.NewCounterFunc(
prometheus.CounterOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
f,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.metrics[key] = counter
m.Unlock()
prometheus.MustRegister(counter)
return counter
}
// NewGauge ...
func (m *Metrics) NewGauge(subsystem, name, help string) prometheus.Gauge {
guage := prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.metrics[key] = guage
m.Unlock()
prometheus.MustRegister(guage)
return guage
}
// NewGaugeFunc ...
func (m *Metrics) NewGaugeFunc(subsystem, name, help string, f func() float64) prometheus.GaugeFunc {
guage := prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
f,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.metrics[key] = guage
m.Unlock()
prometheus.MustRegister(guage)
return guage
}
// NewGaugeVec ...
func (m *Metrics) NewGaugeVec(subsystem, name, help string, labels []string) *prometheus.GaugeVec {
guagevec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
labels,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.guagevecs[key] = guagevec
m.Unlock()
prometheus.MustRegister(guagevec)
return guagevec
}
// NewSummary ...
func (m *Metrics) NewSummary(subsystem, name, help string) prometheus.Summary {
summary := prometheus.NewSummary(
prometheus.SummaryOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
Objectives: DefObjectives,
},
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.metrics[key] = summary
m.Unlock()
prometheus.MustRegister(summary)
return summary
}
// NewSummaryVec ...
func (m *Metrics) NewSummaryVec(subsystem, name, help string, labels []string) *prometheus.SummaryVec {
sumvec := prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
Objectives: DefObjectives,
},
labels,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.Lock()
m.sumvecs[key] = sumvec
m.Unlock()
prometheus.MustRegister(sumvec)
return sumvec
}
// Counter ...
func (m *Metrics) Counter(subsystem, name string) prometheus.Counter {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.metrics[key].(prometheus.Counter)
}
// Gauge ...
func (m *Metrics) Gauge(subsystem, name string) prometheus.Gauge {
key := fmt.Sprintf("%s_%s", subsystem, name)
m.RLock()
defer m.RUnlock()
return m.metrics[key].(prometheus.Gauge)
}
// GaugeVec ...
func (m *Metrics) GaugeVec(subsystem, name string) *prometheus.GaugeVec {
key := fmt.Sprintf("%s_%s", subsystem, name)
m.RLock()
defer m.RUnlock()
return m.guagevecs[key]
}
// Summary ...
func (m *Metrics) Summary(subsystem, name string) prometheus.Summary {
key := fmt.Sprintf("%s_%s", subsystem, name)
m.RLock()
defer m.RUnlock()
return m.metrics[key].(prometheus.Summary)
}
// SummaryVec ...
func (m *Metrics) SummaryVec(subsystem, name string) *prometheus.SummaryVec {
key := fmt.Sprintf("%s_%s", subsystem, name)
m.RLock()
defer m.RUnlock()
return m.sumvecs[key]
}
// Handler ...
func (m *Metrics) Handler() http.Handler {
return promhttp.Handler()
}
// Run ...
func (m *Metrics) Run(addr string) {
http.Handle("/", m.Handler())
log.Infof("metrics endpoint listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}

51
irc/metrics_test.go Normal file
View File

@@ -0,0 +1,51 @@
package irc
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMetrics(t *testing.T) {
assert := assert.New(t)
m := NewMetrics("test")
m.NewCounter("foo", "counter", "help")
m.NewCounterFunc("foo", "counter_func", "help", func() float64 { return 1.0 })
m.NewGauge("foo", "gauge", "help")
m.NewGaugeFunc("foo", "gauge_func", "help", func() float64 { return 1.0 })
m.NewGaugeVec("foo", "gauge_vec", "help", []string{"test"})
m.Counter("foo", "counter").Inc()
m.Gauge("foo", "gauge").Add(1)
m.GaugeVec("foo", "gauge_vec").WithLabelValues("test").Add(1)
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
m.Handler().ServeHTTP(w, r)
assert.Equal(w.Code, http.StatusOK)
assert.Regexp(
`
# HELP test_foo_counter help
# TYPE test_foo_counter counter
test_foo_counter 1
# HELP test_foo_counter_func help
# TYPE test_foo_counter_func counter
test_foo_counter_func 1
# HELP test_foo_gauge help
# TYPE test_foo_gauge gauge
test_foo_gauge 1
# HELP test_foo_gauge_func help
# TYPE test_foo_gauge_func gauge
test_foo_gauge_func 1
# HELP test_foo_gauge_vec help
# TYPE test_foo_gauge_vec gauge
test_foo_gauge_vec{test="test"} 1
`,
w.Body.String(),
)
}

View File

@@ -51,25 +51,26 @@ const (
)
const (
Away UserMode = 'a' // not a real user mode (flag)
Invisible UserMode = 'i'
LocalOperator UserMode = 'O'
Operator UserMode = 'o'
Restricted UserMode = 'r'
ServerNotice UserMode = 's' // deprecated
WallOps UserMode = 'w'
SecureConn UserMode = 'z'
SecureOnly UserMode = 'Z'
Away UserMode = 'a' // not a real user mode (flag)
Invisible UserMode = 'i'
Operator UserMode = 'o'
WallOps UserMode = 'w'
Registered UserMode = 'r' // not a real user mode (flag)
SecureConn UserMode = 'z'
SecureOnly UserMode = 'Z'
HostMask UserMode = 'x'
)
var (
SupportedUserModes = UserModes{
Invisible, Operator,
Invisible, Operator, HostMask,
}
DefaultChannelModes = ChannelModes{
NoOutside, OpOnlyTopic,
}
)
const (
Anonymous ChannelMode = 'a' // flag
BanMask ChannelMode = 'b' // arg
ChannelCreator ChannelMode = 'O' // flag
ChannelOperator ChannelMode = 'o' // arg
@@ -81,17 +82,16 @@ const (
NoOutside ChannelMode = 'n' // flag
OpOnlyTopic ChannelMode = 't' // flag
Private ChannelMode = 'p' // flag
Quiet ChannelMode = 'q' // flag
ReOp ChannelMode = 'r' // flag
Secret ChannelMode = 's' // flag, deprecated
UserLimit ChannelMode = 'l' // flag arg
Voice ChannelMode = 'v' // arg
SecureChan ChannelMode = 'Z' // arg
)
var (
SupportedChannelModes = ChannelModes{
BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside,
OpOnlyTopic, Private, UserLimit,
OpOnlyTopic, Private, UserLimit, Secret, SecureChan,
}
)
@@ -108,7 +108,7 @@ func (m *ModeCommand) HandleServer(s *Server) {
return
}
if client != target && !client.flags[Operator] {
if client != target && !client.modes.Has(Operator) {
client.ErrUsersDontMatch()
return
}
@@ -117,29 +117,27 @@ func (m *ModeCommand) HandleServer(s *Server) {
for _, change := range m.changes {
switch change.mode {
case Invisible, ServerNotice, WallOps, SecureOnly:
case Invisible, HostMask, WallOps, SecureOnly:
switch change.op {
case Add:
if target.flags[change.mode] {
if target.modes.Has(change.mode) {
continue
}
target.flags[change.mode] = true
target.modes.Set(change.mode)
changes = append(changes, change)
case Remove:
if !target.flags[change.mode] {
if !target.modes.Has(change.mode) {
continue
}
delete(target.flags, change.mode)
target.modes.Unset(change.mode)
changes = append(changes, change)
}
case Operator, LocalOperator:
case Operator:
if change.op == Remove {
if !target.flags[change.mode] {
if !target.modes.Has(change.mode) {
continue
}
delete(target.flags, change.mode)
target.modes.Unset(change.mode)
changes = append(changes, change)
}
}

View File

@@ -27,3 +27,28 @@ func LookupHostname(addr Name) Name {
hostname := strings.TrimSuffix(names[0], ".")
return Name(hostname)
}
var allowedHostnameChars = "abcdefghijklmnopqrstuvwxyz1234567890-."
func IsHostname(name string) bool {
// IRC hostnames specifically require a period
if !strings.Contains(name, ".") || len(name) < 1 || len(name) > 253 {
return false
}
// ensure each part of hostname is valid
for _, part := range strings.Split(name, ".") {
if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
return false
}
}
// ensure all chars of hostname are valid
for _, char := range strings.Split(strings.ToLower(name), "") {
if !strings.Contains(allowedHostnameChars, char) {
return false
}
}
return true
}

45
irc/net_test.go Normal file
View File

@@ -0,0 +1,45 @@
package irc
import "testing"
// hostnames from https://github.com/DanielOaks/irc-parser-tests
var (
goodHostnames = []string{
"irc.example.com",
"i.coolguy.net",
"irc-srv.net.uk",
"iRC.CooLguY.NeT",
"gsf.ds342.co.uk",
"324.net.uk",
"xn--bcher-kva.ch",
}
badHostnames = []string{
"-lol-.net.uk",
"-lol.net.uk",
"_irc._sctp.lol.net.uk",
"irc",
"com",
"",
}
)
func TestIsHostname(t *testing.T) {
for _, name := range goodHostnames {
if !IsHostname(name) {
t.Error(
"Expected to pass, but could not validate hostname",
name,
)
}
}
for _, name := range badHostnames {
if IsHostname(name) {
t.Error(
"Expected to fail, but successfully validated hostname",
name,
)
}
}
}

View File

@@ -13,10 +13,6 @@ func (m *NickCommand) HandleRegServer(s *Server) {
return
}
if client.capState == CapNegotiating {
client.capState = CapNegotiated
}
if m.nickname == "" {
client.ErrNoNicknameGiven()
return
@@ -71,7 +67,7 @@ type OperNickCommand struct {
func (msg *OperNickCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.flags[Operator] {
if !client.modes.Has(Operator) {
client.ErrNoPrivileges()
return
}

View File

@@ -1,31 +1,124 @@
package irc
import (
"golang.org/x/crypto/bcrypt"
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"sync"
log "github.com/sirupsen/logrus"
)
var (
EmptyPasswordError = errors.New("empty password")
)
var DefaultPasswordHasher = &Base64BCryptPasswordHasher{}
func GenerateEncodedPassword(passwd string) (encoded string, err error) {
if passwd == "" {
err = EmptyPasswordError
type PasswordHasher interface {
Decode(encoded []byte) (decoded []byte, err error)
Encode(password []byte) (encoded []byte, err error)
Compare(encoded []byte, password []byte) error
}
type PasswordStore interface {
Get(username string) ([]byte, bool)
Set(username, password string) error
Verify(username, password string) error
}
type PasswordStoreOpts struct {
hasher PasswordHasher
}
type MemoryPasswordStore struct {
sync.RWMutex
passwords map[string][]byte
hasher PasswordHasher
}
func NewMemoryPasswordStore(passwords map[string][]byte, opts PasswordStoreOpts) *MemoryPasswordStore {
var hasher PasswordHasher
if opts.hasher != nil {
hasher = opts.hasher
} else {
hasher = DefaultPasswordHasher
}
return &MemoryPasswordStore{
passwords: passwords,
hasher: hasher,
}
}
func (store *MemoryPasswordStore) Get(username string) ([]byte, bool) {
store.RLock()
defer store.RUnlock()
hash, ok := store.passwords[username]
return hash, ok
}
func (store *MemoryPasswordStore) Set(username, password string) error {
// Not Implemented
return nil
}
func (store *MemoryPasswordStore) Verify(username, password string) error {
log.Debugf("looking up: %s", username)
log.Debugf("%v", store.passwords)
hash, ok := store.Get(username)
if !ok {
log.Debugf("username %s not found", username)
return fmt.Errorf("account not found: %s", username)
}
return store.hasher.Compare(hash, []byte(password))
}
type Base64BCryptPasswordHasher struct{}
func (hasher *Base64BCryptPasswordHasher) Decode(encoded []byte) (decoded []byte, err error) {
if encoded == nil {
err = fmt.Errorf("empty password")
return
}
bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
if err != nil {
return
}
encoded = base64.StdEncoding.EncodeToString(bcrypted)
decoded = make([]byte, base64.StdEncoding.DecodedLen(len(encoded)))
log.Debugf("Decode:")
log.Debugf("decoded: %v", decoded)
log.Debugf("encoded: %v", encoded)
_, err = base64.StdEncoding.Decode(decoded, encoded)
return
}
func (hasher *Base64BCryptPasswordHasher) Encode(password []byte) (encoded []byte, err error) {
if password == nil {
err = fmt.Errorf("empty password")
return
}
bcrypted, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
if err != nil {
return
}
base64.StdEncoding.Encode(encoded, bcrypted)
return
}
func (hasher *Base64BCryptPasswordHasher) Compare(encoded, password []byte) error {
log.Debugf("encoded: %s", encoded)
log.Debugf("password: %s", password)
decoded, err := hasher.Decode(encoded)
log.Debugf("decoded: %s", decoded)
log.Debugf("err: %s", err)
if err != nil {
return err
}
return bcrypt.CompareHashAndPassword(decoded, []byte(password))
}
// DEPRECATED
func DecodePassword(encoded string) (decoded []byte, err error) {
if encoded == "" {
err = EmptyPasswordError
err = fmt.Errorf("empty password")
return
}
decoded, err = base64.StdEncoding.DecodeString(encoded)

22
irc/privacy.go Normal file
View File

@@ -0,0 +1,22 @@
package irc
func CanSeeChannel(client *Client, channel *Channel) bool {
isPrivate := channel.flags.Has(Private)
isSecret := channel.flags.Has(Secret)
isMember := channel.members.Has(client)
isOperator := client.modes.Has(Operator)
isRegistered := client.modes.Has(Registered)
isSecure := client.modes.Has(SecureConn)
if !(isSecret || isPrivate) {
return true
}
if isSecret && (isMember || isOperator) {
return true
}
if isPrivate && (isMember || isOperator || (isRegistered && isSecure)) {
return true
}
return false
}

View File

@@ -164,20 +164,27 @@ func RplKill(client *Client, target *Client, comment Text) string {
}
func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string {
return NewStringReply(nil, CAP, "%s %s :%s", client.Nick(), subCommand, arg)
// client.server needs to be here to workaround a parsing bug in weechat 1.4
// and let it connect to the server (otherwise it doesn't respond to the CAP
// message with anything and just hangs on connection)
return NewStringReply(client.server, CAP, "%s %s :%s", client.Nick(), subCommand, arg)
}
// numeric replies
func (target *Client) RplWelcome() {
target.NumericReply(RPL_WELCOME,
":Welcome to the Internet Relay Network %s", target.Id())
target.NumericReply(
RPL_WELCOME,
":Welcome to the %s Internet Relay Network %s",
target.server.Network(),
target.Id(),
)
}
func (target *Client) RplYourHost() {
target.NumericReply(
RPL_YOURHOST,
":Your host is %s, running version %s",
":Your host is %s, running %s",
target.server.name,
FullVersion(),
)
@@ -231,24 +238,48 @@ func (target *Client) RplYoureOper() {
":You are now an IRC operator")
}
// <config file> :Rehashing
func (target *Client) RplRehashing() {
target.NumericReply(
RPL_REHASHING,
"%s :Rehashing",
target.server.config.Name(),
)
}
func (target *Client) RplWhois(client *Client) {
target.RplWhoisUser(client)
if client.flags[Operator] {
if client.modes.Has(Operator) {
target.RplWhoisOperator(client)
}
target.RplWhoisIdle(client)
target.RplWhoisChannels(client)
if client.flags[SecureConn] {
if client.modes.Has(SecureConn) {
target.RplWhoisSecure(client)
}
target.RplEndOfWhois()
target.RplWhoisServer(client)
target.RplWhoisLoggedIn(client)
target.RplEndOfWhois(client)
}
func (target *Client) RplWhoisUser(client *Client) {
target.NumericReply(RPL_WHOISUSER,
"%s %s %s * :%s", client.Nick(), client.username, client.hostname,
client.realname)
var clientHost Name
if target.modes.Has(Operator) || !client.modes.Has(HostMask) {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
target.NumericReply(
RPL_WHOISUSER,
"%s %s %s * :%s",
client.Nick(),
client.username,
clientHost,
client.realname,
)
}
func (target *Client) RplWhoisOperator(client *Client) {
@@ -270,9 +301,35 @@ func (target *Client) RplWhoisIdle(client *Client) {
client.Nick(), client.IdleSeconds(), client.SignonTime())
}
func (target *Client) RplEndOfWhois() {
target.NumericReply(RPL_ENDOFWHOIS,
":End of WHOIS list")
func (target *Client) RplWhoisLoggedIn(client *Client) {
if client.sasl.Id() == "" {
return
}
target.NumericReply(
RPL_WHOISLOGGEDIN,
"%s %s :Is logged in as",
client.Nick(),
client.sasl.Id(),
)
}
func (target *Client) RplWhoisServer(client *Client) {
target.NumericReply(
RPL_WHOISSERVER,
"%s %s :%s",
client.Nick(),
client.Server(),
client.ServerInfo(),
)
}
func (target *Client) RplEndOfWhois(client *Client) {
target.NumericReply(
RPL_ENDOFWHOIS,
"%s :End of WHOIS list",
client.Nick(),
)
}
func (target *Client) RplChannelModeIs(channel *Channel) {
@@ -283,38 +340,55 @@ func (target *Client) RplChannelModeIs(channel *Channel) {
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
// :<hopcount> <real name>
func (target *Client) RplWhoReply(channel *Channel, client *Client) {
var clientHost Name
if target.modes.Has(Operator) || !client.modes.Has(HostMask) {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
channelName := "*"
flags := ""
if client.flags[Away] {
if client.modes.Has(Away) {
flags = "G"
} else {
flags = "H"
}
if client.flags[Operator] {
if client.modes.Has(Operator) {
flags += "*"
}
if channel != nil {
channelName = channel.name.String()
if target.capabilities[MultiPrefix] {
if channel.members[client][ChannelOperator] {
if channel.members.Get(client).Has(ChannelOperator) {
flags += "@"
}
if channel.members[client][Voice] {
if channel.members.Get(client).Has(Voice) {
flags += "+"
}
} else {
if channel.members[client][ChannelOperator] {
if channel.members.Get(client).Has(ChannelOperator) {
flags += "@"
} else if channel.members[client][Voice] {
} else if channel.members.Get(client).Has(Voice) {
flags += "+"
}
}
}
target.NumericReply(RPL_WHOREPLY,
"%s %s %s %s %s %s :%d %s", channelName, client.username, client.hostname,
client.server.name, client.Nick(), flags, client.hops, client.realname)
target.NumericReply(
RPL_WHOREPLY,
"%s %s %s %s %s %s :%d %s",
channelName,
client.username,
clientHost,
client.server.name,
client.Nick(),
flags,
client.hops,
client.realname,
)
}
// <name> :End of WHO list
@@ -415,8 +489,13 @@ func (target *Client) RplMOTDEnd() {
}
func (target *Client) RplList(channel *Channel) {
target.NumericReply(RPL_LIST,
"%s %d :%s", channel, len(channel.members), channel.topic)
target.NumericReply(
RPL_LIST,
"%s %d :%s",
channel,
channel.members.Count(),
channel.topic,
)
}
func (target *Client) RplListEnd(server *Server) {
@@ -430,8 +509,12 @@ func (target *Client) RplNamReply(channel *Channel) {
}
func (target *Client) RplWhoisChannels(client *Client) {
target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS,
"%s :%s", client.Nick())
target.MultilineReply(
client.WhoisChannelsNames(target),
RPL_WHOISCHANNELS,
"%s :%s",
client.Nick(),
)
}
func (target *Client) RplVersion() {
@@ -453,10 +536,94 @@ func (target *Client) RplTime() {
"%s :%s", target.server.name, time.Now().Format(time.RFC1123))
}
func (target *Client) RplLUserClient() {
target.NumericReply(
RPL_LUSERCLIENT,
"There are %d users and %d invisible on %d servers",
// TODO: count global visible users
target.server.clients.Count(),
// TODO: count global invisible users
0,
// TODO: count global server connections
1,
)
}
func (target *Client) RplLUserUnknown() {
nUnknown := target.server.connections.Value() - target.server.clients.Count()
if nUnknown == 0 {
return
}
target.NumericReply(
RPL_LUSERUNKNOWN,
"%d :unknown connections(s)",
nUnknown,
)
}
func (target *Client) RplLUserChannels() {
nChannels := target.server.channels.Count()
if nChannels == 0 {
return
}
target.NumericReply(
RPL_LUSERCHANNELS,
"%d :channel(s) formed",
nChannels,
)
}
func (target *Client) RplLUserOp() {
nOperators := 0
target.server.clients.Range(func(_ Name, client *Client) bool {
if client.modes.Has(Operator) {
nOperators++
}
return true
})
if nOperators == 0 {
return
}
target.NumericReply(
RPL_LUSEROP,
"%d :operator(s) online",
// TODO: state store should know this
nOperators,
)
}
func (target *Client) RplLUserMe() {
target.NumericReply(
RPL_LUSERME,
"I have %d clients and %d servers",
target.server.clients.Count(),
// TODO: count server connections
1,
)
}
func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
target.NumericReply(RPL_WHOWASUSER,
var whoWasHost Name
if target.modes.Has(Operator) {
whoWasHost = whoWas.hostname
} else {
whoWasHost = whoWas.hostmask
}
target.NumericReply(
RPL_WHOWASUSER,
"%s %s %s * :%s",
whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname)
whoWas.nickname,
whoWas.username,
whoWasHost,
whoWas.realname,
)
}
func (target *Client) RplEndOfWhoWas(nickname Name) {
@@ -616,3 +783,84 @@ func (target *Client) ErrInviteOnlyChan(channel *Channel) {
target.NumericReply(ERR_INVITEONLYCHAN,
"%s :Cannot join channel (+i)", channel)
}
//
// SASL Errors / Replies
//
func RplAuthenticate(client *Client, arg string) string {
return NewStringReply(client.server, AUTHENTICATE, arg)
}
func (target *Client) RplLoggedIn(authcid string) {
target.NumericReply(
RPL_LOGGEDIN,
"%s %s :You are now logged in as %s",
target, authcid, authcid,
)
}
func (target *Client) RplLoggedOut() {
target.NumericReply(
RPL_LOGGEDIN,
"%s :You are now logged out",
target,
)
}
func (target *Client) ErrNickLocked() {
target.NumericReply(
ERR_NICKLOCKED,
"%s :You must use a nick assigned to you",
target.Nick(),
)
}
func (target *Client) RplSaslSuccess() {
target.NumericReply(
RPL_SASLSUCCESS,
"%s :SASL authentication successful",
target.Nick(),
)
}
func (target *Client) ErrSaslFail(message string) {
target.NumericReply(
ERR_SASLFAIL,
"%s :SASL authentication failed: %s",
target.Nick(), message,
)
}
func (target *Client) ErrSaslTooLong() {
target.NumericReply(
ERR_SASLFAIL,
"%s :SASL message too long",
target.Nick(),
)
}
func (target *Client) ErrSaslAborted() {
target.NumericReply(
ERR_SASLABORTED,
"%s :SASL authentication aborted",
target.Nick(),
)
}
func (target *Client) ErrSaslAlready() {
target.NumericReply(
ERR_SASLALREADY,
"%s :You have already authenticated using SASL",
target.Nick(),
)
}
func (target *Client) RplSaslMechs(mechs ...string) {
target.NumericReply(
RPL_SASLMECHS,
"%s %s :are available SASL mechanisms",
target.Nick(),
strings.Join(mechs, ","),
)
}

84
irc/sasl.go Normal file
View File

@@ -0,0 +1,84 @@
package irc
import (
"bytes"
"sync"
)
type SaslState struct {
sync.RWMutex
started bool
buffer *bytes.Buffer
mech string
authcid string
}
func NewSaslState() *SaslState {
return &SaslState{buffer: &bytes.Buffer{}}
}
func (s *SaslState) Reset() {
s.Lock()
defer s.Unlock()
s.started = false
s.buffer.Reset()
s.mech = ""
s.authcid = ""
}
func (s *SaslState) Started() bool {
s.RLock()
defer s.RUnlock()
return s.started
}
func (s *SaslState) Start() {
s.Lock()
defer s.Unlock()
s.started = true
}
func (s *SaslState) WriteString(data string) {
s.Lock()
defer s.Unlock()
s.buffer.WriteString(data)
}
func (s SaslState) Len() int {
s.RLock()
defer s.RUnlock()
return s.buffer.Len()
}
func (s *SaslState) String() string {
s.RLock()
defer s.RUnlock()
return s.buffer.String()
}
func (s *SaslState) Login(authcid string) {
s.Lock()
defer s.Unlock()
s.started = false
s.buffer.Reset()
s.mech = ""
s.authcid = authcid
}
func (s *SaslState) Id() string {
s.RLock()
defer s.RUnlock()
return s.authcid
}

View File

@@ -2,16 +2,19 @@ package irc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"log"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
log "github.com/sirupsen/logrus"
)
type ServerCommand interface {
@@ -25,40 +28,61 @@ type RegServerCommand interface {
}
type Server struct {
channels ChannelNameMap
clients *ClientLookupSet
commands chan Command
ctime time.Time
idle chan *Client
motdFile string
name Name
newConns chan net.Conn
operators map[Name][]byte
password []byte
signals chan os.Signal
whoWas *WhoWasList
config *Config
metrics *Metrics
channels *ChannelNameMap
connections *Counter
clients *ClientLookupSet
ctime time.Time
idle chan *Client
motdFile string
name Name
network Name
description string
newConns chan net.Conn
operators map[Name][]byte
accounts PasswordStore
password []byte
signals chan os.Signal
done chan bool
whoWas *WhoWasList
ids map[string]*Identity
}
var (
SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
syscall.SIGTERM, syscall.SIGQUIT}
SERVER_SIGNALS = []os.Signal{
syscall.SIGINT,
syscall.SIGTERM,
}
)
func NewServer(config *Config) *Server {
server := &Server{
channels: make(ChannelNameMap),
clients: NewClientLookupSet(),
commands: make(chan Command),
ctime: time.Now(),
idle: make(chan *Client),
motdFile: config.Server.MOTD,
name: NewName(config.Server.Name),
newConns: make(chan net.Conn),
operators: config.Operators(),
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
whoWas: NewWhoWasList(100),
config: config,
metrics: NewMetrics("eris"),
channels: NewChannelNameMap(),
connections: &Counter{},
clients: NewClientLookupSet(),
ctime: time.Now(),
idle: make(chan *Client),
motdFile: config.Server.MOTD,
name: NewName(config.Server.Name),
network: NewName(config.Network.Name),
description: config.Server.Description,
newConns: make(chan net.Conn),
operators: config.Operators(),
accounts: NewMemoryPasswordStore(config.Accounts(), PasswordStoreOpts{}),
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
done: make(chan bool),
whoWas: NewWhoWasList(100),
ids: make(map[string]*Identity),
}
log.Debugf("accounts: %v", config.Accounts())
// TODO: Make this configureable?
server.ids["global"] = NewIdentity(config.Server.Name, "global")
if config.Server.Password != "" {
server.password = config.Server.PasswordBytes()
}
@@ -73,69 +97,130 @@ func NewServer(config *Config) *Server {
signal.Notify(server.signals, SERVER_SIGNALS...)
// server uptime counter
server.metrics.NewCounterFunc(
"server", "uptime",
"Number of seconds the server has been running",
func() float64 {
return float64(time.Since(server.ctime).Nanoseconds())
},
)
// client commands counter
server.metrics.NewCounter(
"client", "commands",
"Number of client commands processed",
)
// client messages counter
server.metrics.NewCounter(
"client", "messages",
"Number of client messages exchanged",
)
// server connections gauge
server.metrics.NewGaugeFunc(
"server", "connections",
"Number of active connections to the server",
func() float64 {
return float64(server.connections.Value())
},
)
// server registered (clients) gauge
server.metrics.NewGaugeFunc(
"server", "registered",
"Number of registered clients connected",
func() float64 {
return float64(server.clients.Count())
},
)
// server clients gauge (by secure/insecure)
server.metrics.NewGaugeVec(
"server", "clients",
"Number of registered clients connected (by secure/insecure)",
[]string{"secure"},
)
// server channels gauge
server.metrics.NewGaugeFunc(
"server", "channels",
"Number of active channels",
func() float64 {
return float64(server.channels.Count())
},
)
// client command processing time summaries
server.metrics.NewSummaryVec(
"client", "command_duration_seconds",
"Client command processing time in seconds",
[]string{"command"},
)
// client ping latency summary
server.metrics.NewSummary(
"client", "ping_latency_seconds",
"Client ping latency in seconds",
)
go server.metrics.Run(":9314")
return server
}
func loadChannelList(channel *Channel, list string, maskMode ChannelMode) {
if list == "" {
return
}
channel.lists[maskMode].AddAll(NewNames(strings.Split(list, " ")))
func (server *Server) Wallops(message string) {
text := NewText(message)
server.clients.Range(func(_ Name, client *Client) bool {
if client.modes.Has(WallOps) {
server.metrics.Counter("client", "messages").Inc()
client.replies <- RplNotice(server, client, text)
}
return true
})
}
func (server *Server) processCommand(cmd Command) {
client := cmd.Client()
func (server *Server) Wallopsf(format string, args ...interface{}) {
server.Wallops(fmt.Sprintf(format, args...))
}
if !client.registered {
regCmd, ok := cmd.(RegServerCommand)
if !ok {
client.Quit("unexpected command")
return
}
regCmd.HandleRegServer(server)
return
}
func (server *Server) Global(message string) {
text := NewText(message)
server.clients.Range(func(_ Name, client *Client) bool {
server.metrics.Counter("client", "messages").Inc()
client.replies <- RplNotice(server.ids["global"], client, text)
return true
})
}
srvCmd, ok := cmd.(ServerCommand)
if !ok {
client.ErrUnknownCommand(cmd.Code())
return
}
switch srvCmd.(type) {
case *PingCommand, *PongCommand:
client.Touch()
case *QuitCommand:
// no-op
default:
client.Active()
client.Touch()
}
srvCmd.HandleServer(server)
func (server *Server) Globalf(format string, args ...interface{}) {
server.Global(fmt.Sprintf(format, args...))
}
func (server *Server) Shutdown() {
for _, client := range server.clients.byNick {
client.Reply(RplNotice(server, client, "shutting down"))
}
server.Global("shutting down...")
}
func (server *Server) Stop() {
server.done <- true
}
func (server *Server) Run() {
done := false
for !done {
for {
select {
case <-server.done:
return
case <-server.signals:
server.Shutdown()
done = true
// Give at least 1s for clients to see the shutdown
go func() {
time.Sleep(1 * time.Second)
server.Stop()
}()
case conn := <-server.newConns:
NewClient(server, conn)
case cmd := <-server.commands:
server.processCommand(cmd)
go NewClient(server, conn)
case client := <-server.idle:
client.Idle()
@@ -147,11 +232,18 @@ func (s *Server) acceptor(listener net.Listener) {
for {
conn, err := listener.Accept()
if err != nil {
Log.error.Printf("%s accept error: %s", s, err)
log.Errorf("%s accept error: %s", s, err)
continue
}
Log.debug.Printf("%s accept: %s", s, conn.RemoteAddr())
log.Debugf("%s accept: %s", s, conn.RemoteAddr())
if _, ok := conn.(*tls.Conn); ok {
s.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Inc()
} else {
s.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Inc()
}
s.connections.Inc()
s.newConns <- conn
}
}
@@ -166,7 +258,7 @@ func (s *Server) listen(addr string) {
log.Fatal(s, "listen error: ", err)
}
Log.info.Printf("%s listening on %s", s, addr)
log.Infof("%s listening on %s", s, addr)
go s.acceptor(listener)
}
@@ -187,7 +279,7 @@ func (s *Server) listentls(addr string, tlsconfig *TLSConfig) {
log.Fatalf("error binding to %s: %s", addr, err)
}
Log.info.Printf("%s listening on %s", s, addr)
log.Infof("%s listening on %s (TLS)", s, addr)
go s.acceptor(listener)
}
@@ -207,6 +299,11 @@ func (s *Server) tryRegister(c *Client) {
c.RplYourHost()
c.RplCreated()
c.RplMyInfo()
lusers := LUsersCommand{}
lusers.SetClient(c)
lusers.HandleServer(s)
s.MOTD(c)
}
@@ -237,10 +334,29 @@ func (server *Server) MOTD(client *Client) {
client.RplMOTDEnd()
}
func (s *Server) Rehash() error {
err := s.config.Reload()
if err != nil {
return err
}
s.motdFile = s.config.Server.MOTD
s.name = NewName(s.config.Server.Name)
s.network = NewName(s.config.Network.Name)
s.description = s.config.Server.Description
s.operators = s.config.Operators()
return nil
}
func (s *Server) Id() Name {
return s.name
}
func (s *Server) Network() Name {
return s.network
}
func (s *Server) String() string {
return s.name.String()
}
@@ -292,18 +408,106 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
flags := msg.Flags()
if len(flags) > 0 {
for _, mode := range flags {
client.flags[mode] = true
client.modes.Set(mode)
}
client.RplUModeIs(client)
}
msg.setUserInfo(server)
}
func (msg *AuthenticateCommand) HandleRegServer(server *Server) {
client := msg.Client()
if !client.authorized {
client.ErrPasswdMismatch()
client.Quit("bad password")
return
}
if msg.arg == "*" {
client.ErrSaslAborted()
return
}
if !client.sasl.Started() {
if msg.arg == "PLAIN" {
client.sasl.Start()
client.Reply(RplAuthenticate(client, "+"))
} else {
client.RplSaslMechs("PLAIN")
client.ErrSaslFail("Unknown authentication mechanism")
}
return
}
if len(msg.arg) > 400 {
client.ErrSaslTooLong()
return
}
if len(msg.arg) == 400 {
client.sasl.WriteString(msg.arg)
return
}
if msg.arg != "+" {
client.sasl.WriteString(msg.arg)
}
data, err := base64.StdEncoding.DecodeString(client.sasl.String())
if err != nil {
client.ErrSaslFail("Invalid base64 encoding")
client.sasl.Reset()
return
}
// Do authentication
var (
authcid string
authzid string
password string
)
tokens := bytes.Split(data, []byte{'\000'})
if len(tokens) == 3 {
authcid = string(tokens[0])
authzid = string(tokens[1])
password = string(tokens[2])
if authzid == "" {
authzid = authcid
} else if authzid != authcid {
client.ErrSaslFail("authzid and authcid should be the same")
return
}
} else {
client.ErrSaslFail("invalid authentication blob")
return
}
err = server.accounts.Verify(authcid, password)
if err != nil {
client.ErrSaslFail("invalid authentication")
return
}
client.sasl.Login(authcid)
client.RplLoggedIn(authcid)
client.RplSaslSuccess()
client.modes.Set(Registered)
client.Reply(
RplModeChanges(
client, client,
ModeChanges{
&ModeChange{mode: Registered, op: Add},
},
),
)
}
func (msg *UserCommand) setUserInfo(server *Server) {
client := msg.Client()
if client.capState == CapNegotiating {
client.capState = CapNegotiated
}
server.clients.Remove(client)
client.username, client.realname = msg.username, msg.realname
@@ -330,7 +534,8 @@ func (m *PingCommand) HandleServer(s *Server) {
}
func (m *PongCommand) HandleServer(s *Server) {
// no-op
v := s.metrics.Summary("client", "ping_latency_seconds")
v.Observe(time.Now().Sub(m.Client().pingTime).Seconds())
}
func (m *UserCommand) HandleServer(s *Server) {
@@ -345,9 +550,10 @@ func (m *JoinCommand) HandleServer(s *Server) {
client := m.Client()
if m.zero {
for channel := range client.channels {
client.channels.Range(func(channel *Channel) bool {
channel.Part(client, client.Nick().Text())
}
return true
})
return
}
@@ -359,7 +565,7 @@ func (m *JoinCommand) HandleServer(s *Server) {
channel := s.channels.Get(name)
if channel == nil {
channel = NewChannel(s, name)
channel = NewChannel(s, name, true)
}
channel.Join(client, key)
}
@@ -416,28 +622,34 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) {
client.ErrCannotSendToUser(target.nick, "secure connection required")
return
}
server.metrics.Counter("client", "messages").Inc()
target.Reply(RplPrivMsg(client, target, msg.message))
if target.flags[Away] {
if target.modes.Has(Away) {
client.RplAway(target)
}
}
func (client *Client) WhoisChannelsNames() []string {
chstrs := make([]string, len(client.channels))
func (client *Client) WhoisChannelsNames(target *Client) []string {
chstrs := make([]string, client.channels.Count())
index := 0
for channel := range client.channels {
client.channels.Range(func(channel *Channel) bool {
if !CanSeeChannel(target, channel) {
return true
}
switch {
case channel.members[client][ChannelOperator]:
case channel.members.Get(client).Has(ChannelOperator):
chstrs[index] = "@" + channel.name.String()
case channel.members[client][Voice]:
case channel.members.Get(client).Has(Voice):
chstrs[index] = "+" + channel.name.String()
default:
chstrs[index] = channel.name.String()
}
index += 1
}
index++
return true
})
return chstrs
}
@@ -448,22 +660,24 @@ func (m *WhoisCommand) HandleServer(server *Server) {
for _, mask := range m.masks {
matches := server.clients.FindAll(mask)
if len(matches) == 0 {
if matches.Count() == 0 {
client.ErrNoSuchNick(mask)
continue
}
for mclient := range matches {
matches.Range(func(mclient *Client) bool {
client.RplWhois(mclient)
}
return true
})
}
}
func whoChannel(client *Client, channel *Channel, friends ClientSet) {
for member := range channel.members {
if !client.flags[Invisible] || friends[client] {
func whoChannel(client *Client, channel *Channel, friends *ClientSet) {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
if !client.modes.Has(Invisible) || friends.Has(client) {
client.RplWhoReply(channel, member)
}
}
return true
})
}
func (msg *WhoCommand) HandleServer(server *Server) {
@@ -472,9 +686,10 @@ func (msg *WhoCommand) HandleServer(server *Server) {
mask := msg.mask
if mask == "" {
for _, channel := range server.channels {
server.channels.Range(func(name Name, channel *Channel) bool {
whoChannel(client, channel, friends)
}
return true
})
} else if mask.IsChannel() {
// TODO implement wildcard matching
channel := server.channels.Get(mask)
@@ -482,9 +697,11 @@ func (msg *WhoCommand) HandleServer(server *Server) {
whoChannel(client, channel, friends)
}
} else {
for mclient := range server.clients.FindAll(mask) {
matches := server.clients.FindAll(mask)
matches.Range(func(mclient *Client) bool {
client.RplWhoReply(nil, mclient)
}
return true
})
}
client.RplEndOfWho(mask)
@@ -498,20 +715,50 @@ func (msg *OperCommand) HandleServer(server *Server) {
return
}
client.flags[Operator] = true
client.modes.Set(Operator)
client.modes.Set(WallOps)
client.RplYoureOper()
client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{
mode: Operator,
op: Add,
}}))
client.Reply(
RplModeChanges(
client, client,
ModeChanges{
&ModeChange{mode: Operator, op: Add},
&ModeChange{mode: WallOps, op: Add},
},
),
)
}
func (msg *RehashCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.modes.Has(Operator) {
client.ErrNoPrivileges()
return
}
server.Wallopsf(
"Rehashing server config (%s)",
client.Nick(),
)
err := server.Rehash()
if err != nil {
server.Wallopsf(
"ERROR: Rehashing config failed (%s)",
err,
)
return
}
client.RplRehashing()
}
func (msg *AwayCommand) HandleServer(server *Server) {
client := msg.Client()
if len(msg.text) > 0 {
client.flags[Away] = true
client.modes.Set(Away)
} else {
delete(client.flags, Away)
client.modes.Unset(Away)
}
client.awayMessage = msg.text
}
@@ -535,6 +782,12 @@ func (msg *MOTDCommand) HandleServer(server *Server) {
func (msg *NoticeCommand) HandleServer(server *Server) {
client := msg.Client()
if msg.target == "*" && client.modes.Has(Operator) {
server.Global(msg.message.String())
return
}
if msg.target.IsChannel() {
channel := server.channels.Get(msg.target)
if channel == nil {
@@ -556,6 +809,7 @@ func (msg *NoticeCommand) HandleServer(server *Server) {
client.ErrCannotSendToUser(target.nick, "secure connection required")
return
}
server.metrics.Counter("client", "messages").Inc()
target.Reply(RplNotice(client, target, msg.message))
}
@@ -588,16 +842,17 @@ func (msg *ListCommand) HandleServer(server *Server) {
}
if len(msg.channels) == 0 {
for _, channel := range server.channels {
if !client.flags[Operator] && channel.flags[Private] {
continue
server.channels.Range(func(name Name, channel *Channel) bool {
if !CanSeeChannel(client, channel) {
return true
}
client.RplList(channel)
}
return true
})
} else {
for _, chname := range msg.channels {
channel := server.channels.Get(chname)
if channel == nil || (!client.flags[Operator] && channel.flags[Private]) {
if channel == nil || !CanSeeChannel(client, channel) {
client.ErrNoSuchChannel(chname)
continue
}
@@ -609,10 +864,11 @@ func (msg *ListCommand) HandleServer(server *Server) {
func (msg *NamesCommand) HandleServer(server *Server) {
client := msg.Client()
if len(server.channels) == 0 {
for _, channel := range server.channels {
if server.channels.Count() == 0 {
server.channels.Range(func(name Name, channel *Channel) bool {
channel.Names(client)
}
return true
})
return
}
@@ -664,9 +920,29 @@ func (msg *TimeCommand) HandleServer(server *Server) {
client.RplTime()
}
func (msg *LUsersCommand) HandleServer(server *Server) {
client := msg.Client()
client.RplLUserClient()
client.RplLUserOp()
client.RplLUserUnknown()
client.RplLUserChannels()
client.RplLUserMe()
}
func (msg *WallopsCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.modes.Has(Operator) {
client.ErrNoPrivileges()
return
}
server.Wallops(msg.message.String())
}
func (msg *KillCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.flags[Operator] {
if !client.modes.Has(Operator) {
client.ErrNoPrivileges()
return
}

View File

@@ -4,6 +4,9 @@ import (
"bufio"
"io"
"net"
"sync"
log "github.com/sirupsen/logrus"
)
const (
@@ -12,10 +15,11 @@ const (
)
type Socket struct {
closed bool
conn net.Conn
scanner *bufio.Scanner
writer *bufio.Writer
closed bool
closedMutex sync.RWMutex
conn net.Conn
scanner *bufio.Scanner
writer *bufio.Writer
}
func NewSocket(conn net.Conn) *Socket {
@@ -31,15 +35,20 @@ func (socket *Socket) String() string {
}
func (socket *Socket) Close() {
socket.closedMutex.Lock()
defer socket.closedMutex.Unlock()
if socket.closed {
return
}
socket.closed = true
socket.conn.Close()
Log.debug.Printf("%s closed", socket)
log.Debugf("%s closed", socket)
}
func (socket *Socket) Read() (line string, err error) {
socket.closedMutex.RLock()
defer socket.closedMutex.RUnlock()
if socket.closed {
err = io.EOF
return
@@ -50,7 +59,7 @@ func (socket *Socket) Read() (line string, err error) {
if len(line) == 0 {
continue
}
Log.debug.Printf("%s → %s", socket, line)
log.Debugf("%s → %s", socket, line)
return
}
@@ -63,6 +72,8 @@ func (socket *Socket) Read() (line string, err error) {
}
func (socket *Socket) Write(line string) (err error) {
socket.closedMutex.RLock()
defer socket.closedMutex.RUnlock()
if socket.closed {
err = io.EOF
return
@@ -80,14 +91,14 @@ func (socket *Socket) Write(line string) (err error) {
return
}
Log.debug.Printf("%s ← %s", socket, line)
log.Debugf("%s ← %s", socket, line)
return
}
func (socket *Socket) isError(err error, dir rune) bool {
if err != nil {
if err != io.EOF {
Log.debug.Printf("%s %c error: %s", socket, dir, err)
log.Debugf("%s %c error: %s", socket, dir, err)
}
return true
}

View File

@@ -10,7 +10,7 @@ import (
var (
// regexps
ChannelNameExpr = regexp.MustCompile(`^[&!#+][\pL\pN]{1,63}$`)
NicknameExpr = regexp.MustCompile("^[\\pL\\pN\\pP\\pS]{1,32}$")
NicknameExpr = regexp.MustCompile(`^[\pL\pN\pP\pS]{1,32}$`)
)
// Names are normalized and canonicalized to remove formatting marks
@@ -38,10 +38,24 @@ func (name Name) IsChannel() bool {
func (name Name) IsNickname() bool {
namestr := name.String()
// * is used for unregistered clients
// * is used for mask matching
// ? is used for mask matching
// . is used to denote server names
// , is used as a separator by the protocol
// ! separates username from nickname
// @ separates nick+user from hostname
// # is a channel prefix
// @+ are channel membership prefixes
if namestr == "*" || strings.Contains(namestr, ",") || strings.Contains("#@+", string(namestr[0])) {
// ~&@%+ are channel membership prefixes
// - is typically disallowed from first char of nicknames
// nicknames can't start with digits
if strings.Contains(namestr, "*") || strings.Contains(namestr, "?") ||
strings.Contains(namestr, ".") || strings.Contains(namestr, ",") ||
strings.Contains(namestr, "!") || strings.Contains(namestr, "@") ||
strings.Contains("#~&@%+-1234567890", string(namestr[0])) {
return false
}
// names that look like hostnames are restricted to servers, as with other ircds
if IsHostname(namestr) {
return false
}
return NicknameExpr.MatchString(namestr)

View File

@@ -3,110 +3,391 @@ package irc
import (
"fmt"
"strings"
"sync"
)
//
// simple types
//
type ChannelNameMap map[Name]*Channel
func (channels ChannelNameMap) Get(name Name) *Channel {
return channels[name.ToLower()]
type Counter struct {
sync.RWMutex
value int
}
func (channels ChannelNameMap) Add(channel *Channel) error {
if channels[channel.name.ToLower()] != nil {
func (c *Counter) Inc() {
c.Lock()
defer c.Unlock()
c.value++
}
func (c *Counter) Dec() {
c.Lock()
defer c.Unlock()
c.value--
}
func (c *Counter) Value() int {
c.RLock()
defer c.RUnlock()
return c.value
}
// ChannelNameMap holds a mapping of channel names to *Channel structs
// that is safe for concurrent readers and writers.
type ChannelNameMap struct {
sync.RWMutex
channels map[Name]*Channel
}
// NewChannelNameMap returns a new initialized *ChannelNameMap
func NewChannelNameMap() *ChannelNameMap {
return &ChannelNameMap{
channels: make(map[Name]*Channel),
}
}
// Count returns the number of *Channel9s)
func (c *ChannelNameMap) Count() int {
c.RLock()
defer c.RUnlock()
return len(c.channels)
}
// Range ranges of the *Channels(s) calling f
func (c *ChannelNameMap) Range(f func(kay Name, value *Channel) bool) {
c.Lock()
defer c.Unlock()
for k, v := range c.channels {
if !f(k, v) {
return
}
}
}
// Get returns a *Channel given a name if it exists or a zero-value *Channel
func (c *ChannelNameMap) Get(name Name) *Channel {
c.RLock()
defer c.RUnlock()
return c.channels[name.ToLower()]
}
// Add adds a new *Channel if not already exists or an error otherwise
func (c *ChannelNameMap) Add(channel *Channel) error {
c.Lock()
defer c.Unlock()
if c.channels[channel.name.ToLower()] != nil {
return fmt.Errorf("%s: already set", channel.name)
}
channels[channel.name.ToLower()] = channel
c.channels[channel.name.ToLower()] = channel
return nil
}
func (channels ChannelNameMap) Remove(channel *Channel) error {
if channel != channels[channel.name.ToLower()] {
// Remove removes a *Channel if it exists or an error otherwise
func (c *ChannelNameMap) Remove(channel *Channel) error {
c.Lock()
defer c.Unlock()
if channel != c.channels[channel.name.ToLower()] {
return fmt.Errorf("%s: mismatch", channel.name)
}
delete(channels, channel.name.ToLower())
delete(c.channels, channel.name.ToLower())
return nil
}
type ChannelModeSet map[ChannelMode]bool
// UserModeSet holds a mapping of channel modes
type UserModeSet struct {
sync.RWMutex
modes map[UserMode]bool
}
func (set ChannelModeSet) String() string {
if len(set) == 0 {
// NewUserModeSet returns a new UserModeSet
func NewUserModeSet() *UserModeSet {
return &UserModeSet{modes: make(map[UserMode]bool)}
}
// Set sets mode
func (set *UserModeSet) Set(mode UserMode) {
set.Lock()
defer set.Unlock()
set.modes[mode] = true
}
// Unset unsets mode
func (set *UserModeSet) Unset(mode UserMode) {
set.Lock()
defer set.Unlock()
delete(set.modes, mode)
}
// Has returns true if the mode is set
func (set *UserModeSet) Has(mode UserMode) bool {
set.RLock()
defer set.RUnlock()
ok, _ := set.modes[mode]
return ok
}
// Range ranges of the modes calling f
func (set *UserModeSet) Range(f func(mode UserMode) bool) {
set.RLock()
defer set.RUnlock()
for mode := range set.modes {
if !f(mode) {
return
}
}
}
// String returns a string representing the channel modes
func (set *UserModeSet) String() string {
set.RLock()
defer set.RUnlock()
if len(set.modes) == 0 {
return ""
}
strs := make([]string, len(set))
strs := make([]string, len(set.modes))
index := 0
for mode := range set {
for mode := range set.modes {
strs[index] = mode.String()
index += 1
index++
}
return strings.Join(strs, "")
}
type ClientSet map[*Client]bool
func (clients ClientSet) Add(client *Client) {
clients[client] = true
// ChannelModeSet holds a mapping of channel modes
type ChannelModeSet struct {
sync.RWMutex
modes map[ChannelMode]bool
}
func (clients ClientSet) Remove(client *Client) {
delete(clients, client)
// NewChannelModeSet returns a new ChannelModeSet
func NewChannelModeSet() *ChannelModeSet {
return &ChannelModeSet{modes: make(map[ChannelMode]bool)}
}
func (clients ClientSet) Has(client *Client) bool {
return clients[client]
// Set sets mode
func (set *ChannelModeSet) Set(mode ChannelMode) {
set.Lock()
defer set.Unlock()
set.modes[mode] = true
}
type MemberSet map[*Client]ChannelModeSet
func (members MemberSet) Add(member *Client) {
members[member] = make(ChannelModeSet)
// Unset unsets mode
func (set *ChannelModeSet) Unset(mode ChannelMode) {
set.Lock()
defer set.Unlock()
delete(set.modes, mode)
}
func (members MemberSet) Remove(member *Client) {
delete(members, member)
}
func (members MemberSet) Has(member *Client) bool {
_, ok := members[member]
// Has returns true if the mode is set
func (set *ChannelModeSet) Has(mode ChannelMode) bool {
set.RLock()
defer set.RUnlock()
ok, _ := set.modes[mode]
return ok
}
func (members MemberSet) HasMode(member *Client, mode ChannelMode) bool {
modes, ok := members[member]
// Range ranges of the modes calling f
func (set *ChannelModeSet) Range(f func(mode ChannelMode) bool) {
set.RLock()
defer set.RUnlock()
for mode := range set.modes {
if !f(mode) {
return
}
}
}
// String returns a string representing the channel modes
func (set *ChannelModeSet) String() string {
set.RLock()
defer set.RUnlock()
if len(set.modes) == 0 {
return ""
}
strs := make([]string, len(set.modes))
index := 0
for mode := range set.modes {
strs[index] = mode.String()
index++
}
return strings.Join(strs, "")
}
type ClientSet struct {
sync.RWMutex
clients map[*Client]bool
}
func NewClientSet() *ClientSet {
return &ClientSet{clients: make(map[*Client]bool)}
}
func (set *ClientSet) Add(client *Client) {
set.Lock()
defer set.Unlock()
set.clients[client] = true
}
func (set *ClientSet) Remove(client *Client) {
set.Lock()
defer set.Unlock()
delete(set.clients, client)
}
func (set *ClientSet) Count() int {
set.RLock()
defer set.RUnlock()
return len(set.clients)
}
func (set *ClientSet) Has(client *Client) bool {
set.RLock()
defer set.RUnlock()
ok, _ := set.clients[client]
return ok
}
func (set *ClientSet) Range(f func(client *Client) bool) {
set.RLock()
defer set.RUnlock()
for client := range set.clients {
if !f(client) {
return
}
}
}
type MemberSet struct {
sync.RWMutex
members map[*Client]*ChannelModeSet
}
func NewMemberSet() *MemberSet {
return &MemberSet{members: make(map[*Client]*ChannelModeSet)}
}
func (set *MemberSet) Count() int {
set.RLock()
defer set.RUnlock()
return len(set.members)
}
func (set *MemberSet) Range(f func(client *Client, modes *ChannelModeSet) bool) {
set.RLock()
defer set.RUnlock()
for client, modes := range set.members {
if !f(client, modes) {
break
}
}
}
func (set *MemberSet) Add(member *Client) {
set.Lock()
defer set.Unlock()
set.members[member] = NewChannelModeSet()
}
func (set *MemberSet) Remove(member *Client) {
set.Lock()
defer set.Unlock()
delete(set.members, member)
}
func (set *MemberSet) Has(member *Client) bool {
set.RLock()
defer set.RUnlock()
_, ok := set.members[member]
return ok
}
func (set *MemberSet) Get(member *Client) *ChannelModeSet {
set.RLock()
defer set.RUnlock()
return set.members[member]
}
func (set *MemberSet) HasMode(member *Client, mode ChannelMode) bool {
set.RLock()
defer set.RUnlock()
modes, ok := set.members[member]
if !ok {
return false
}
return modes[mode]
return modes.Has(mode)
}
func (members MemberSet) AnyHasMode(mode ChannelMode) bool {
for _, modes := range members {
if modes[mode] {
return true
type ChannelSet struct {
sync.RWMutex
channels map[*Channel]bool
}
func NewChannelSet() *ChannelSet {
return &ChannelSet{channels: make(map[*Channel]bool)}
}
func (set *ChannelSet) Count() int {
set.RLock()
defer set.RUnlock()
return len(set.channels)
}
func (set *ChannelSet) Add(channel *Channel) {
set.Lock()
defer set.Unlock()
set.channels[channel] = true
}
func (set *ChannelSet) Remove(channel *Channel) {
set.Lock()
defer set.Unlock()
delete(set.channels, channel)
}
func (set *ChannelSet) Range(f func(channel *Channel) bool) {
set.RLock()
defer set.RUnlock()
for channel := range set.channels {
if !f(channel) {
break
}
}
return false
}
type ChannelSet map[*Channel]bool
func (channels ChannelSet) Add(channel *Channel) {
channels[channel] = true
type Identity struct {
nickname string
username string
hostname string
}
func (channels ChannelSet) Remove(channel *Channel) {
delete(channels, channel)
}
func NewIdentity(hostname string, args ...string) *Identity {
id := &Identity{hostname: hostname}
func (channels ChannelSet) First() *Channel {
for channel := range channels {
return channel
if len(args) > 0 {
id.nickname = args[0]
}
return nil
if len(args) > 2 {
id.username = args[1]
} else {
id.username = id.nickname
}
return id
}
func (id *Identity) Id() Name {
return NewName(id.username)
}
func (id *Identity) Nick() Name {
return NewName(id.nickname)
}
func (id *Identity) String() string {
return fmt.Sprintf("%s!%s@%s", id.nickname, id.username, id.hostname)
}
//

11
irc/utils.go Normal file
View File

@@ -0,0 +1,11 @@
package irc
import (
"crypto/sha256"
"fmt"
)
func SHA256(data string) string {
hash := sha256.Sum256([]byte(data))
return fmt.Sprintf("%x", hash)
}

View File

@@ -1,11 +1,15 @@
package irc
var (
// Version release version
Version = "1.5.1"
import (
"fmt"
)
// Build will be overwritten automatically by the build system
Build = "-dev"
var (
// Package package name
Package = "eris"
// Version release version
Version = "1.6.4"
// GitCommit will be overwritten automatically by the build system
GitCommit = "HEAD"
@@ -13,5 +17,5 @@ var (
// FullVersion display the full version and build
func FullVersion() string {
return Version + Build + " (" + GitCommit + ")"
return fmt.Sprintf("%s-%s@%s", Package, Version, GitCommit)
}

View File

@@ -1,6 +1,11 @@
package irc
import (
"sync"
)
type WhoWasList struct {
sync.RWMutex
buffer []*WhoWas
start int
end int
@@ -10,6 +15,7 @@ type WhoWas struct {
nickname Name
username Name
hostname Name
hostmask Name
realname Text
}
@@ -20,10 +26,13 @@ func NewWhoWasList(size uint) *WhoWasList {
}
func (list *WhoWasList) Append(client *Client) {
list.Lock()
defer list.Unlock()
list.buffer[list.end] = &WhoWas{
nickname: client.Nick(),
username: client.username,
hostname: client.hostname,
hostmask: client.hostmask,
realname: client.realname,
}
list.end = (list.end + 1) % len(list.buffer)
@@ -33,6 +42,8 @@ func (list *WhoWasList) Append(client *Client) {
}
func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas {
list.RLock()
defer list.RUnlock()
results := make([]*WhoWas, 0)
for whoWas := range list.Each() {
if nickname != whoWas.nickname {
@@ -47,6 +58,8 @@ func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas {
}
func (list *WhoWasList) prev(index int) int {
list.RLock()
defer list.RUnlock()
index -= 1
if index < 0 {
index += len(list.buffer)
@@ -58,6 +71,8 @@ func (list *WhoWasList) prev(index int) int {
func (list *WhoWasList) Each() <-chan *WhoWas {
ch := make(chan *WhoWas)
go func() {
list.RLock()
defer list.RUnlock()
defer close(ch)
if list.start == list.end {
return

View File

@@ -1,31 +1,42 @@
network:
# network name
name: Local
server:
# server name
name: localhost
# server name
name: localhost.localdomain
# addresses to listen on
listen:
- ":6667"
# server description
description: Local Server
# addresses to listen on for TLS
tlslisten:
":6697":
key: key.pem
cert: cert.pem
# addresses to listen on
listen:
- ":6667"
# password to login to the server
# generated using "ircd genpasswd"
#password: ""
# addresses to listen on for TLS
tlslisten:
":6697":
key: key.pem
cert: cert.pem
# log level, one of error, warn, info, debug
log: debug
# password to login to the server
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
#password: ""
# motd filename
motd: ircd.motd
# motd filename
motd: ircd.motd
# ircd operators
# irc operators
operator:
# operator named 'admin' with password 'password'
admin:
# password to login with /OPER command
# generated using "ircd genpasswd"
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
# operator named 'admin' with password 'password'
admin:
# password to login with /OPER command
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
# accounts (SASL)
account:
# username 'admin'
admin:
# password 'admin'
password: JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD

27
key.pem
View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAqwXZqhKHtR9vRgIkb628kIZmE95wr46JVsX/CLt5frpokiPV
4ub2/dXTTryU01kOqXSeBNvR0jUig6xwPZghV2gWJ20E59Py8+O1PMuHsbheq5Uw
vy0br2LtW60e9C03/qQsW/6ZkH9sdm5CxrWq9OpWNtCcnmn9R4Z3R031tiBo11ey
7QLdhtTrzdeGmR3BTy10uU481FXfJFk7wvjtu6FEVgyFDzLRT1soeLdqQQXNFCmE
higzySNHK+8sh84oXJWP9ZSSSmxBZIBO4METOnzhQb6M6i5xGvZyT1swYFiiHK+7
p7N3whElyMCJDWlptJ62V15MK5q9dKeOvyCu2QIDAQABAoIBACteSWsKVeWFOidr
pae4S+fuXPZO4w5xu7wIa3rgZ5fOV3QvqC5o2VtXHMWLpsXNgJ1SGDwtLhD7Lmxr
ju7fpBzRJUUMLUFIiwRzyuLGzfswQLmMFJd2Ld4U3RHxXbcXvGpTYXhWoMR2u59w
qHhRWy/OONyfnAfD4tl/bMHlMBjlxuO5vOcbMhMkVqD4bQ+9FXLyTDr+IZHiqm55
AiEi8nyHbldgB4BohyJ1su+GiWLNPGPcUeV+wW2Z7RoLcALkZB+J649DV8JIf2Pv
ZlR3bn0b6O7jYzj5xdIV1aAY4+mplKsJltT8ibw3YFmKSm2x6Q4WAi/RCYSOoTJj
ogQ6b/ECgYEA1/TYLOj+Uott1/xEBGkHVBc3DyU0qM2bPWqwnTp2SILvGwO+U1cN
HQql0+QO3SuqW5Eb9Iu6gvP43IQR6KwolHkIR7snx0yfWw9t6/BOT/jYaU4XWBZu
jIppbAvm5JXKLrSf1WFogKWwvWGtlsbN0Gk+RJ4BJ7d+Ec/k8VeZZw0CgYEAyrwS
HT1dk1dbxj00GGfZGaaw1FBU97SnEE8OSstosgPQqdcs1LfijK4e7zvkmPRp65Zr
m6BlqpqcAr2Lf937ZLkFRzlhBh4w8E62kwCAxASutiv/CeAC42F3SzaNmtIokDPa
kps1rmBuE/GH/KSZH2MFXwqpuejzEWMYaMFnc/0CgYAEX2nNJv6XT0lvGSWLbVD/
q+SqgtdZH6ioCrP+ywNAHp6WznGZIGcckm2fJ7wBUHQHnJ2TSw0Av83nMSlnq6y6
rOS1Bx4cE/oaDurr3xbG2cQQBcvFNdcRM3BFBsyrDjlkzrV5zZofGLvGaebo6aeb
M7ZWD1j4o+eCltIM7PgnNQKBgBkEQhv3kY937Sw2KzKDxgdjSVi5nPKACUl65+GU
3hNxYxNCKRcKpdsENW8B7gBt0JAwnJC0pzb5Ix03dHpP0xCnwB3815sgqJtOqzrS
ihEmHsT+AteeG90hDs5qKekb28OHkoYavvIIlizB1iz3xqlX17bVowH829meZ8mt
a+2lAoGAWZlPVXckm6s9QofCr2MT1GCZGrA7+QGeHmU0t9HnZD2wFxaPRqq6B0s5
EMVueez/h0hhsJb3rYU/Pm1U3ehpowL0Nao413nGf0S8rmzKdLzzCr0+hSGeFZwy
svXrFjXeupvOv/FbLJjFc7jSnqz+shwnHknnVUjgMWoiHxu40h4=
-----END RSA PRIVATE KEY-----

69
main.go
View File

@@ -1,58 +1,47 @@
package main
import (
"flag"
"fmt"
"log"
"syscall"
"os"
"github.com/docopt/docopt-go"
"github.com/prologic/ircd/irc"
"golang.org/x/crypto/ssh/terminal"
log "github.com/sirupsen/logrus"
"github.com/mmcloughlin/professor"
"github.com/prologic/eris/irc"
)
func main() {
version := irc.FullVersion()
usage := `ircd.
Usage:
ircd genpasswd [--conf <filename>]
ircd run [--conf <filename>]
ircd -h | --help
ircd -v | --version
Options:
-c --conf <filename> Configuration file to use [default: ircd.yml].
-h --help Show this screen.
-v --version Show version.`
var (
version bool
debug bool
configfile string
)
arguments, _ := docopt.Parse(usage, nil, true, version, false)
flag.BoolVar(&version, "v", false, "display version information")
flag.BoolVar(&debug, "d", false, "enable debug logging")
flag.StringVar(&configfile, "c", "ircd.yml", "config file")
flag.Parse()
// Special case -- We do not need to load the config file here
if arguments["genpasswd"].(bool) {
fmt.Print("Enter Password: ")
bytePassword, err := terminal.ReadPassword(syscall.Stdin)
if err != nil {
log.Fatal("Error reading password:", err.Error())
}
password := string(bytePassword)
encoded, err := irc.GenerateEncodedPassword(password)
if err != nil {
log.Fatalln("encoding error:", err)
}
fmt.Print("\n")
fmt.Println(encoded)
return
if version {
fmt.Printf(irc.FullVersion())
os.Exit(0)
}
if debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.WarnLevel)
}
if debug {
go professor.Launch(":6060")
}
configfile := arguments["--conf"].(string)
config, err := irc.LoadConfig(configfile)
if err != nil {
log.Fatal("Config file did not load successfully:", err.Error())
}
if arguments["run"].(bool) {
irc.Log.SetLevel(config.Server.Log)
server := irc.NewServer(config)
log.Println(irc.FullVersion(), "running")
defer log.Println(irc.FullVersion(), "exiting")
server.Run()
}
irc.NewServer(config).Run()
}

577
main_test.go Normal file
View File

@@ -0,0 +1,577 @@
package main
import (
"crypto/sha256"
"flag"
"fmt"
"os"
"strings"
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/renstrom/shortuuid"
"github.com/stretchr/testify/assert"
"github.com/thoj/go-ircevent"
eris "github.com/prologic/eris/irc"
)
const (
TIMEOUT = 3 * time.Second
)
var (
server *eris.Server
debug = flag.Bool("d", false, "enable debug logging")
)
func setupServer() *eris.Server {
config := &eris.Config{}
config.Network.Name = "Test"
config.Server.Name = "test"
config.Server.Description = "Test"
config.Server.Listen = []string{":6667"}
// SASL
config.Account = map[string]*eris.PassConfig{
"admin": &eris.PassConfig{"JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD"},
}
server := eris.NewServer(config)
go server.Run()
return server
}
func randomValidName() string {
var name eris.Name
for {
name = eris.NewName(shortuuid.New())
if name.IsNickname() {
break
}
}
return name.String()
}
func newClient(start bool) *irc.Connection {
name := randomValidName()
client := irc.IRC(name, name)
client.RealName = fmt.Sprintf("Test Client: %s", name)
err := client.Connect("localhost:6667")
if err != nil {
log.Fatalf("error setting up test client: %s", err)
}
if start {
go client.Loop()
}
return client
}
func TestMain(m *testing.M) {
flag.Parse()
if *debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.WarnLevel)
}
server = setupServer()
result := m.Run()
server.Stop()
os.Exit(result)
}
func TestConnection(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client := newClient(false)
client.AddCallback("001", func(e *irc.Event) {
actual <- true
})
defer client.Quit()
go client.Loop()
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestSASL(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client := newClient(false)
client.SASLLogin = "admin"
client.SASLPassword = "admin"
client.AddCallback("001", func(e *irc.Event) {
actual <- true
})
defer client.Quit()
go client.Loop()
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestRplWelcome(t *testing.T) {
assert := assert.New(t)
expected := "Welcome to the .* Internet Relay Network .*!.*@.*"
actual := make(chan string)
client := newClient(false)
client.AddCallback("001", func(e *irc.Event) {
actual <- e.Message()
})
defer client.Quit()
go client.Loop()
select {
case res := <-actual:
assert.Regexp(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestUser_JOIN(t *testing.T) {
assert := assert.New(t)
client := newClient(false)
expected := []string{client.GetNick(), "=", "#join", fmt.Sprintf("@%s", client.GetNick())}
actual := make(chan string)
client.AddCallback("353", func(e *irc.Event) {
for i := range e.Arguments {
actual <- e.Arguments[i]
}
})
defer client.Quit()
go client.Loop()
client.Join("#join")
client.SendRaw("NAMES #join")
for i := range expected {
select {
case res := <-actual:
assert.Equal(expected[i], res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
}
func TestChannel_InviteOnly(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(false)
client2 := newClient(false)
client1.AddCallback("324", func(e *irc.Event) {
if strings.Contains(e.Arguments[2], "i") {
client2.Join("#inviteonly")
} else {
client1.Mode("#inviteonly")
}
})
client2.AddCallback("473", func(e *irc.Event) {
actual <- true
})
client2.AddCallback("JOIN", func(e *irc.Event) {
actual <- false
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
client1.Join("#inviteonly")
client1.Mode("#inviteonly", "+i")
client1.Mode("#inviteonly")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestUser_WithHostMask(t *testing.T) {
assert := assert.New(t)
client1 := newClient(false)
client2 := newClient(false)
expected := fmt.Sprintf("%x", sha256.Sum256([]byte("localhost")))
actual := make(chan string)
client1.AddCallback("001", func(e *irc.Event) {
client1.Mode(client1.GetNick(), "+x")
})
client2.AddCallback("001", func(e *irc.Event) {
client2.Whois(client1.GetNick())
})
client2.AddCallback("401", func(e *irc.Event) {
client2.Whois(client1.GetNick())
})
client2.AddCallback("311", func(e *irc.Event) {
actual <- e.Arguments[3]
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
/* FIXME: This test is racey :/
func TestUser_WithoutHostMask(t *testing.T) {
assert := assert.New(t)
client1 := newClient(false)
client2 := newClient(false)
expected := "localhost"
actual := make(chan string)
client1.AddCallback("001", func(e *irc.Event) {
client1.Mode(client1.GetNick(), "-x")
})
client2.AddCallback("001", func(e *irc.Event) {
client2.Whois(client1.GetNick())
})
client2.AddCallback("401", func(e *irc.Event) {
client2.Whois(client1.GetNick())
})
client2.AddCallback("311", func(e *irc.Event) {
actual <- e.Arguments[3]
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
*/
func TestUser_PRIVMSG(t *testing.T) {
assert := assert.New(t)
expected := "Hello World!"
actual := make(chan string)
client1 := newClient(false)
client2 := newClient(false)
client1.AddCallback("001", func(e *irc.Event) {
client1.Privmsg(client2.GetNick(), expected)
})
client1.AddCallback("PRIVMSG", func(e *irc.Event) {
actual <- e.Message()
})
client2.AddCallback("001", func(e *irc.Event) {
client2.Privmsg(client1.GetNick(), expected)
})
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
actual <- e.Message()
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_PRIVMSG(t *testing.T) {
assert := assert.New(t)
expected := "Hello World!"
actual := make(chan string)
client1 := newClient(false)
client2 := newClient(false)
client1.AddCallback("JOIN", func(e *irc.Event) {
client1.Privmsg(e.Arguments[0], expected)
})
client2.AddCallback("JOIN", func(e *irc.Event) {
client2.Privmsg(e.Arguments[0], expected)
})
client1.AddCallback("PRIVMSG", func(e *irc.Event) {
actual <- e.Message()
})
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
actual <- e.Message()
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
client1.Join("#channelprivmsg")
client2.Join("#channelprivmsg")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_NoExternal(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(true)
client2 := newClient(true)
client1.AddCallback("JOIN", func(e *irc.Event) {
channel := e.Arguments[0]
if channel == "#noexternal" {
if e.Nick == client1.GetNick() {
client2.Privmsg("#noexternal", "FooBar!")
} else {
assert.Fail(fmt.Sprintf("unexpected user %s joined %s", e.Nick, channel))
}
} else {
assert.Fail(fmt.Sprintf("unexpected channel %s", channel))
}
})
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
if e.Arguments[0] == "#noexternal" {
actual <- false
}
})
client2.AddCallback("404", func(e *irc.Event) {
actual <- true
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
client1.Join("#noexternal")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_SetTopic_InvalidChannel(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(false)
client1.AddCallback("403", func(e *irc.Event) {
actual <- true
})
defer client1.Quit()
go client1.Loop()
client1.SendRaw("TOPIC #invalidchannel :FooBar")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_SetTopic_NotOnChannel(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(false)
client2 := newClient(false)
client1.AddCallback("442", func(e *irc.Event) {
actual <- true
})
client2.AddCallback("JOIN", func(e *irc.Event) {
client1.SendRaw("TOPIC #notonchannel :FooBar")
})
defer client1.Quit()
go client1.Loop()
client2.Join("#notonchannel")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_BadChannelKey(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(false)
client2 := newClient(false)
client1.AddCallback("324", func(e *irc.Event) {
if strings.Contains(e.Arguments[2], "k") {
client2.Join(e.Arguments[1])
} else {
client1.Mode("#badchannelkey")
}
})
client2.AddCallback("JOIN", func(e *irc.Event) {
if e.Nick == client2.GetNick() && e.Arguments[0] == "#badchannelkey" {
actual <- false
}
})
client2.AddCallback("475", func(e *irc.Event) {
actual <- true
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
client1.Join("#badchannelkey")
client1.Mode("#badchannelkey", "+k", "opensesame")
client1.Mode("#badchannelkey")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}
func TestChannel_GoodChannelKey(t *testing.T) {
assert := assert.New(t)
expected := true
actual := make(chan bool)
client1 := newClient(true)
client2 := newClient(true)
client1.AddCallback("324", func(e *irc.Event) {
if strings.Contains(e.Arguments[2], "k") {
client2.SendRawf("JOIN %s :opensesame", e.Arguments[1])
} else {
client1.Mode("#goodchannelkey")
}
})
client2.AddCallback("JOIN", func(e *irc.Event) {
if e.Nick == client2.GetNick() && e.Arguments[0] == "#goodchannelkey" {
actual <- true
}
})
client2.AddCallback("475", func(e *irc.Event) {
actual <- false
})
defer client1.Quit()
defer client2.Quit()
go client1.Loop()
go client2.Loop()
client1.Join("#goodchannelkey")
client1.Mode("#goodchannelkey", "+k", "opensesame")
client1.Mode("#goodchannelkey")
select {
case res := <-actual:
assert.Equal(expected, res)
case <-time.After(TIMEOUT):
assert.Fail("timeout")
}
}

41
scripts/release.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
echo -n "Version to tag: "
read TAG
echo -n "Name of release: "
read NAME
echo -n "Desc of release: "
read DESC
git tag ${TAG}
git push --tags
if [ ! -d ./bin ]; then
mkdir bin
else
rm -rf ./bin/*
fi
echo -n "Building binaries ... "
GOOS=linux GOARCH=amd64 go build -o ./bin/eris-Linux-x86_64 .
GOOS=linux GOARCH=arm64 go build -o ./bin/eris-Linux-arm_64 .
GOOS=darwin GOARCH=amd64 go build -o ./bin/eris-Darwin-x86_64 .
GOOS=windows GOARCH=amd64 go build -o ./bin/eris-Windows-x86_64.exe .
echo "DONE"
echo -n "Uploading binaries ... "
github-release release \
-u prologic -p -r eris \
-t ${TAG} -n "${NAME}" -d "${DESC}"
for file in bin/*; do
name="$(echo $file | sed -e 's|bin/||g')"
github-release upload -u prologic -r eris -t ${TAG} -n $name -f $file
done
echo "DONE"