70 Commits

Author SHA1 Message Date
James Mills
0e07999cb2 Add TestSASL 2017-12-06 02:12:31 -08:00
James Mills
79c60eafaf Added some topic tests 2017-12-06 02:02:23 -08:00
James Mills
0ec9aaa5eb Fix all tests :D yay 2017-12-06 01:49:15 -08:00
James Mills
90910a20b1 Removed use of deadlock sync package 2017-12-03 23:30:20 -08:00
James Mills
43e4726461 Update vendored 3rd-party deps 2017-12-03 20:34:48 -08:00
James Mills
493b58459d Bah humbut 2017-12-03 20:22:23 -08:00
James Mills
d53b08b3a0 Added TestChannel_BadChannelKey and TestChannel_GoodChannelKey tests 2017-12-03 18:06:16 -08:00
James Mills
8ba7a2c1e2 Added TestChannel_NoExternal test 2017-12-03 17:41:56 -08:00
James Mills
aed917ea2f Fuck it 2017-12-03 17:28:55 -08:00
James Mills
94bbfe4baf Try to fix TestChannel_PRIVMSG test 2017-12-03 16:11:23 -08:00
James Mills
8732f70630 Refactored integration testing framework for better timtouts and added TestChannel_InviteOnly test 2017-12-03 14:34:31 -08:00
James Mills
b7f798d600 Added TestChannel_PRIVMSG test 2017-12-03 13:01:11 -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
49 changed files with 2303 additions and 365 deletions

View File

@@ -1,17 +1,23 @@
workspace:
base: /go
path: src/github.com/prologic/ircd
path: src/github.com/prologic/eris
pipeline:
build:
image: golang
commands:
- go get -d
- go get -d ./...
- go build .
test:
image: golang
commands:
- go get -d ./...
- go test ./...
docker:
image: plugins/docker
repo: r.mills.io/prologic/ircd
repo: r.mills.io/prologic/eris
registry: r.mills.io
secrets: [ docker_username, docker_password ]
@@ -21,7 +27,7 @@ pipeline:
from: drone@mills.io
skip_verify: true
when:
status: [ changed, failure ]
status: [ success, changed, failure ]
secrets:
registry_username:

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*~*
bin
*.db
*.pem

36
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "vendor/github.com/docopt/docopt-go"]
path = vendor/github.com/docopt/docopt-go
url = https://github.com/docopt/docopt-go
[submodule "vendor/github.com/sirupsen/logrus"]
path = vendor/github.com/sirupsen/logrus
url = https://github.com/sirupsen/logrus
@@ -25,3 +22,36 @@
[submodule "vendor/gopkg.in/yaml.v2"]
path = vendor/gopkg.in/yaml.v2
url = https://gopkg.in/yaml.v2
[submodule "vendor/github.com/prometheus/client_golang"]
path = vendor/github.com/prometheus/client_golang
url = https://github.com/prometheus/client_golang
[submodule "vendor/github.com/beorn7/perks"]
path = vendor/github.com/beorn7/perks
url = https://github.com/beorn7/perks
[submodule "vendor/github.com/golang/protobuf"]
path = vendor/github.com/golang/protobuf
url = https://github.com/golang/protobuf
[submodule "vendor/github.com/prometheus/client_model"]
path = vendor/github.com/prometheus/client_model
url = https://github.com/prometheus/client_model
[submodule "vendor/github.com/prometheus/common"]
path = vendor/github.com/prometheus/common
url = https://github.com/prometheus/common
[submodule "vendor/github.com/matttproud/golang_protobuf_extensions"]
path = vendor/github.com/matttproud/golang_protobuf_extensions
url = https://github.com/matttproud/golang_protobuf_extensions
[submodule "vendor/github.com/prometheus/procfs"]
path = vendor/github.com/prometheus/procfs
url = https://github.com/prometheus/procfs
[submodule "vendor/github.com/thoj/go-ircevent"]
path = vendor/github.com/thoj/go-ircevent
url = https://github.com/thoj/go-ircevent
[submodule "vendor/github.com/stretchr/testify"]
path = vendor/github.com/stretchr/testify
url = https://github.com/stretchr/testify
[submodule "vendor/github.com/renstrom/shortuuid"]
path = vendor/github.com/renstrom/shortuuid
url = https://github.com/renstrom/shortuuid
[submodule "vendor/github.com/satori/go.uuid"]
path = vendor/github.com/satori/go.uuid
url = https://github.com/satori/go.uuid

8
.travis.yml Normal file
View File

@@ -0,0 +1,8 @@
language: go
sudo: false
go:
- tip
before_install:
- go get github.com/mattn/goveralls
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

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

@@ -27,4 +27,4 @@ COPY --from=build /go/src/github.com/${REPO}/${APP} /${APP}
EXPOSE 6667/tcp 6697/tcp
ENTRYPOINT ["/eris"]
CMD ["run"]
CMD [""]

View File

@@ -6,7 +6,7 @@ APP=eris
PACKAGE=irc
REPO?=prologic/$(APP)
TAG?=latest
BUILD?=-dev
BUILD?=dev
all: dev

View File

@@ -1,5 +1,11 @@
# eris - IRC Server / Daemon written in Go
[![Build Status](https://travis-ci.org/prologic/eris.svg)](https://travis-ci.org/prologic/eris)
[![Go Report Card](https://goreportcard.com/badge/github.com/prologic/eris)](https://goreportcard.com/report/github.com/prologic/eris)
[![Coverage](https://coveralls.io/repos/prologic/eris/badge.svg)](https://coveralls.io/r/prologic/eris)
[![GoDoc](https://godoc.org/github.com/prologic/eris?status.svg)](https://godoc.org/github.com/prologic/eris)
[![Wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/prologic/eris/wiki)
> 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)
> but the upstream project was ultimately shutdown.
@@ -26,7 +32,12 @@ Pull requests and issues are welcome.
Discussion at:
* /server irc.mills.io:6697 (*use SSL*)
* /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
@@ -42,6 +53,42 @@ 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
@@ -53,18 +100,55 @@ $ 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
$ eris 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
$ eris run
$ go install github.com/prologic/mksslcert
$ mksslcert
```
## Deployment
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 Mobile clients
* [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*)
## License
eris is licensed under the MIT License.

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

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
@@ -19,20 +19,20 @@ type Channel struct {
// string, which must be unique on the server.
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[mode] = true
channel.flags.Set(mode)
}
}
@@ -42,7 +42,7 @@ func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
}
func (channel *Channel) IsEmpty() bool {
return len(channel.members) == 0
return channel.members.Count() == 0
}
func (channel *Channel) Names(client *Client) {
@@ -56,26 +56,29 @@ func (channel *Channel) ClientIsOperator(client *Client) bool {
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
}
@@ -106,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
@@ -126,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 {
@@ -151,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)
}
@@ -187,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)
}
@@ -213,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
}
@@ -221,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.flags[SecureConn] {
return false
}
return true
}
@@ -246,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,
@@ -263,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
@@ -304,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
@@ -362,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:
@@ -428,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
})
}
}
@@ -440,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)
@@ -472,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
}
@@ -489,8 +507,8 @@ 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)

View File

@@ -20,20 +20,23 @@ type Client struct {
awayMessage Text
capabilities CapabilitySet
capState CapState
channels ChannelSet
channels *ChannelSet
ctime time.Time
flags map[UserMode]bool
hasQuit bool
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
commands chan Command
replies chan string
username Name
}
@@ -44,21 +47,22 @@ func NewClient(server *Server, conn net.Conn) *Client {
authorized: len(server.password) == 0,
capState: CapNone,
capabilities: make(CapabilitySet),
channels: make(ChannelSet),
channels: NewChannelSet(),
ctime: now,
flags: make(map[UserMode]bool),
sasl: NewSaslState(),
server: server,
socket: NewSocket(conn),
commands: make(chan Command),
replies: make(chan string),
}
if _, ok := conn.(*tls.Conn); ok {
client.flags[SecureConn] = true
client.flags[SecureOnly] = true
}
client.Touch()
go client.run()
go client.writeloop()
go client.readloop()
return client
}
@@ -67,13 +71,26 @@ func NewClient(server *Server, conn net.Conn) *Client {
// command goroutine
//
func (client *Client) run() {
func (client *Client) writeloop() {
for {
select {
case reply, ok := <-client.replies:
if !ok || reply == "" || client.socket == nil {
return
}
client.socket.Write(reply)
}
}
}
func (client *Client) readloop() {
var command Command
var err error
var line string
// Set the hostname for this client.
client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr())
client.hostmask = NewName(SHA256(client.hostname.String()))
for err == nil {
if line, err = client.socket.Read(); err != nil {
@@ -126,6 +143,13 @@ func (client *Client) processCommand(cmd Command) {
return
}
client.server.metrics.Counter("client", "commands").Inc()
defer func(t time.Time) {
v := client.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:
client.Touch()
@@ -176,6 +200,7 @@ func (client *Client) Touch() {
}
func (client *Client) Idle() {
client.pingTime = time.Now()
client.Reply(RplPing(client.server))
if client.quitTimer == nil {
@@ -196,13 +221,20 @@ func (client *Client) Register() {
func (client *Client) destroy() {
// clean up channels
for channel := range client.channels {
client.channels.Range(func(channel *Channel) bool {
channel.Quit(client)
}
return true
})
// clean up server
client.server.connections -= 1
if _, ok := client.socket.conn.(*tls.Conn); ok {
client.server.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Dec()
} else {
client.server.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Dec()
}
client.server.connections.Dec()
client.server.clients.Remove(client)
// clean up self
@@ -214,6 +246,9 @@ func (client *Client) destroy() {
client.quitTimer.Stop()
}
close(client.replies)
client.replies = nil
client.socket.Close()
log.Debugf("%s: destroyed", client)
@@ -259,11 +294,14 @@ func (c *Client) ModeString() (str string) {
return
}
func (c *Client) UserHost() Name {
func (c *Client) UserHost(cloacked bool) Name {
username := "*"
if c.HasUsername() {
username = c.username.String()
}
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))
}
@@ -283,21 +321,23 @@ 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)
func (client *Client) Friends() *ClientSet {
friends := NewClientSet()
friends.Add(client)
for channel := range client.channels {
for member := range channel.members {
client.channels.Range(func(channel *Channel) bool {
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
friends.Add(member)
}
}
return true
})
return true
})
return friends
}
@@ -317,13 +357,16 @@ func (client *Client) ChangeNickname(nickname Name) {
client.server.whoWas.Append(client)
client.nick = nickname
client.server.clients.Add(client)
for friend := range client.Friends() {
client.Friends().Range(func(friend *Client) bool {
friend.Reply(reply)
}
return true
})
}
func (client *Client) Reply(reply string) error {
return client.socket.Write(reply)
func (client *Client) Reply(reply string) {
if client.replies != nil {
client.replies <- reply
}
}
func (client *Client) Quit(message Text) {
@@ -338,10 +381,11 @@ func (client *Client) Quit(message Text) {
friends.Remove(client)
client.destroy()
if len(friends) > 0 {
if friends.Count() > 0 {
reply := RplQuit(client, message)
for friend := range friends {
friends.Range(func(friend *Client) bool {
friend.Reply(reply)
}
return true
})
}
}

View File

@@ -29,12 +29,12 @@ func ExpandUserHost(userhost Name) (expanded Name) {
type ClientLookupSet struct {
sync.RWMutex
byNick map[Name]*Client
nicks map[Name]*Client
}
func NewClientLookupSet() *ClientLookupSet {
return &ClientLookupSet{
byNick: make(map[Name]*Client),
nicks: make(map[Name]*Client),
}
}
@@ -42,14 +42,14 @@ func (clients *ClientLookupSet) Count() int {
clients.RLock()
defer clients.RUnlock()
return len(clients.byNick)
return len(clients.nicks)
}
func (clients *ClientLookupSet) Get(nick Name) *Client {
clients.RLock()
defer clients.RUnlock()
return clients.byNick[nick.ToLower()]
return clients.nicks[nick.ToLower()]
}
func (clients *ClientLookupSet) Add(client *Client) error {
@@ -63,7 +63,7 @@ func (clients *ClientLookupSet) Add(client *Client) error {
clients.Lock()
defer clients.Unlock()
clients.byNick[client.Nick().ToLower()] = client
clients.nicks[client.Nick().ToLower()] = client
return nil
}
@@ -78,22 +78,32 @@ func (clients *ClientLookupSet) Remove(client *Client) error {
clients.Lock()
defer clients.Unlock()
delete(clients.byNick, client.nick.ToLower())
delete(clients.nicks, client.nick.ToLower())
return nil
}
func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) {
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
}
}
}
func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
clients.RLock()
defer clients.RUnlock()
set = make(ClientSet)
set := NewClientSet()
userhost = ExpandUserHost(userhost)
matcher := ircmatch.MakeMatch(userhost.String())
var casemappedNickMask string
for _, client := range clients.byNick {
casemappedNickMask = client.UserHost().String()
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
set.Add(client)
}
@@ -110,8 +120,8 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client {
matcher := ircmatch.MakeMatch(userhost.String())
var casemappedNickMask string
for _, client := range clients.byNick {
casemappedNickMask = client.UserHost().String()
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
return client
}

View File

@@ -26,36 +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,
REHASH: ParseRehashCommand,
PART: ParsePartCommand,
PASS: ParsePassCommand,
PING: ParsePingCommand,
PONG: ParsePongCommand,
PRIVMSG: ParsePrivMsgCommand,
QUIT: ParseQuitCommand,
TIME: ParseTimeCommand,
LUSERS: ParseLUsersCommand,
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,
}
)
@@ -182,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 {
@@ -875,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

@@ -31,6 +31,10 @@ type Config struct {
sync.Mutex
filename string
Network struct {
Name string
}
Server struct {
PassConfig `yaml:",inline"`
Listen []string
@@ -42,6 +46,7 @@ type Config struct {
}
Operator map[string]*PassConfig
Account map[string]*PassConfig
}
func (conf *Config) Operators() map[Name][]byte {
@@ -52,6 +57,14 @@ 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
}
@@ -86,6 +99,10 @@ func LoadConfig(filename string) (config *Config, err error) {
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")
}

View File

@@ -5,37 +5,39 @@ const (
MAX_REPLY_LEN = 512 - len(CRLF)
// string codes
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"
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
@@ -91,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
@@ -177,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
)

194
irc/metrics.go Normal file
View File

@@ -0,0 +1,194 @@
package irc
import (
"fmt"
"net/http"
log "github.com/sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var DefObjectives = map[float64]float64{
0.50: 0.05,
0.90: 0.01,
0.95: 0.005,
0.99: 0.001,
}
type Metrics struct {
namespace string
metrics map[string]prometheus.Metric
gaugevecs map[string]*prometheus.GaugeVec
sumvecs map[string]*prometheus.SummaryVec
}
func NewMetrics(namespace string) *Metrics {
return &Metrics{
namespace: namespace,
metrics: make(map[string]prometheus.Metric),
gaugevecs: make(map[string]*prometheus.GaugeVec),
sumvecs: make(map[string]*prometheus.SummaryVec),
}
}
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.metrics[key] = counter
prometheus.MustRegister(counter)
return counter
}
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.metrics[key] = counter
prometheus.MustRegister(counter)
return counter
}
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.metrics[key] = guage
prometheus.MustRegister(guage)
return guage
}
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.metrics[key] = guage
prometheus.MustRegister(guage)
return guage
}
func (m *Metrics) NewGaugeVec(subsystem, name, help string, labels []string) *prometheus.GaugeVec {
gauagevec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
labels,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.gaugevecs[key] = gauagevec
prometheus.MustRegister(gauagevec)
return gauagevec
}
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.metrics[key] = summary
prometheus.MustRegister(summary)
return summary
}
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.sumvecs[key] = sumvec
prometheus.MustRegister(sumvec)
return sumvec
}
func (m *Metrics) Counter(subsystem, name string) prometheus.Counter {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.metrics[key].(prometheus.Counter)
}
func (m *Metrics) Gauge(subsystem, name string) prometheus.Gauge {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.metrics[key].(prometheus.Gauge)
}
func (m *Metrics) GaugeVec(subsystem, name string) *prometheus.GaugeVec {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.gaugevecs[key]
}
func (m *Metrics) Summary(subsystem, name string) prometheus.Summary {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.metrics[key].(prometheus.Summary)
}
func (m *Metrics) SummaryVec(subsystem, name string) *prometheus.SummaryVec {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.sumvecs[key]
}
func (m *Metrics) Handler() http.Handler {
return promhttp.Handler()
}
func (m *Metrics) Run(addr string) {
http.Handle("/", m.Handler())
log.Infof("metrics endpoint listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}

View File

@@ -51,15 +51,13 @@ 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'
)
var (
@@ -72,7 +70,6 @@ var (
)
const (
Anonymous ChannelMode = 'a' // flag
BanMask ChannelMode = 'b' // arg
ChannelCreator ChannelMode = 'O' // flag
ChannelOperator ChannelMode = 'o' // arg
@@ -84,17 +81,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,
}
)
@@ -120,7 +116,7 @@ func (m *ModeCommand) HandleServer(s *Server) {
for _, change := range m.changes {
switch change.mode {
case Invisible, ServerNotice, WallOps, SecureOnly:
case Invisible, WallOps, SecureOnly:
switch change.op {
case Add:
if target.flags[change.mode] {
@@ -137,7 +133,7 @@ func (m *ModeCommand) HandleServer(s *Server) {
changes = append(changes, change)
}
case Operator, LocalOperator:
case Operator:
if change.op == Remove {
if !target.flags[change.mode] {
continue

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.flags[Operator]
isRegistered := client.flags[Registered]
isSecure := client.flags[SecureConn]
if !(isSecret || isPrivate) {
return true
}
if isSecret && (isMember || isOperator) {
return true
}
if isPrivate && (isMember || isOperator || (isRegistered && isSecure)) {
return true
}
return false
}

View File

@@ -173,14 +173,18 @@ func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string {
// 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 eris version %s",
":Your host is %s, running %s",
target.server.name,
FullVersion(),
)
@@ -255,13 +259,27 @@ func (target *Client) RplWhois(client *Client) {
target.RplWhoisSecure(client)
}
target.RplWhoisServer(client)
target.RplEndOfWhois()
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.flags[Operator] {
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) {
@@ -283,6 +301,19 @@ func (target *Client) RplWhoisIdle(client *Client) {
client.Nick(), client.IdleSeconds(), client.SignonTime())
}
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,
@@ -293,9 +324,12 @@ func (target *Client) RplWhoisServer(client *Client) {
)
}
func (target *Client) RplEndOfWhois() {
target.NumericReply(RPL_ENDOFWHOIS,
":End of WHOIS list")
func (target *Client) RplEndOfWhois(client *Client) {
target.NumericReply(
RPL_ENDOFWHOIS,
"%s :End of WHOIS list",
client.Nick(),
)
}
func (target *Client) RplChannelModeIs(channel *Channel) {
@@ -306,6 +340,14 @@ 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.flags[Operator] {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
channelName := "*"
flags := ""
@@ -321,23 +363,32 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) {
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
@@ -438,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) {
@@ -453,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() {
@@ -490,7 +550,7 @@ func (target *Client) RplLUserClient() {
}
func (target *Client) RplLUserUnknown() {
nUnknown := target.server.connections - len(target.server.clients.byNick)
nUnknown := target.server.connections.Value() - target.server.clients.Count()
if nUnknown == 0 {
return
@@ -504,7 +564,7 @@ func (target *Client) RplLUserUnknown() {
}
func (target *Client) RplLUserChannels() {
nChannels := target.server.channels.Length()
nChannels := target.server.channels.Count()
if nChannels == 0 {
return
}
@@ -518,11 +578,12 @@ func (target *Client) RplLUserChannels() {
func (target *Client) RplLUserOp() {
nOperators := 0
for _, client := range target.server.clients.byNick {
target.server.clients.Range(func(_ Name, client *Client) bool {
if client.flags[Operator] {
nOperators += 1
nOperators++
}
}
return true
})
if nOperators == 0 {
return
@@ -547,9 +608,22 @@ func (target *Client) RplLUserMe() {
}
func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
target.NumericReply(RPL_WHOWASUSER,
var whoWasHost Name
if target.flags[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) {
@@ -709,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,8 +2,10 @@ package irc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
"os"
@@ -27,42 +29,60 @@ type RegServerCommand interface {
type Server struct {
config *Config
metrics *Metrics
channels *ChannelNameMap
connections int
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.SIGHUP,
syscall.SIGTERM, syscall.SIGQUIT,
}
)
func NewServer(config *Config) *Server {
server := &Server{
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 configurabel?
server.ids["global"] = NewIdentity(config.Server.Name, "global")
if config.Server.Password != "" {
server.password = config.Server.PasswordBytes()
}
@@ -77,34 +97,127 @@ 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/insecire)
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 (server *Server) Wallops(message string) {
for _, client := range server.clients.byNick {
text := NewText(message)
server.clients.Range(func(_ Name, client *Client) bool {
if client.flags[WallOps] {
client.Reply(RplNotice(server, client, NewText(message)))
server.metrics.Counter("client", "messages").Inc()
client.replies <- RplNotice(server, client, text)
}
}
return true
})
}
func (server *Server) Wallopsf(format string, args ...interface{}) {
server.Wallops(fmt.Sprintf(format, args...))
}
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
})
}
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:
go NewClient(server, conn)
@@ -124,7 +237,13 @@ func (s *Server) acceptor(listener net.Listener) {
}
log.Debugf("%s accept: %s", s, conn.RemoteAddr())
s.connections += 1
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
}
}
@@ -223,6 +342,7 @@ func (s *Server) Rehash() error {
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()
@@ -233,6 +353,10 @@ 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()
}
@@ -291,6 +415,97 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
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.flags[Registered] = true
client.Reply(
RplModeChanges(
client, client,
ModeChanges{
&ModeChange{mode: Registered, op: Add},
},
),
)
}
func (msg *UserCommand) setUserInfo(server *Server) {
client := msg.Client()
@@ -319,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) {
@@ -334,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
}
@@ -405,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] {
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
}
@@ -437,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.flags[Invisible] || friends.Has(client) {
client.RplWhoReply(channel, member)
}
}
return true
})
}
func (msg *WhoCommand) HandleServer(server *Server) {
@@ -472,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)
@@ -489,11 +716,17 @@ func (msg *OperCommand) HandleServer(server *Server) {
}
client.flags[Operator] = true
client.flags[WallOps] = true
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) {
@@ -549,6 +782,12 @@ func (msg *MOTDCommand) HandleServer(server *Server) {
func (msg *NoticeCommand) HandleServer(server *Server) {
client := msg.Client()
if msg.target == "*" && client.flags[Operator] {
server.Global(msg.message.String())
return
}
if msg.target.IsChannel() {
channel := server.channels.Get(msg.target)
if channel == nil {
@@ -570,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))
}
@@ -603,16 +843,16 @@ func (msg *ListCommand) HandleServer(server *Server) {
if len(msg.channels) == 0 {
server.channels.Range(func(name Name, channel *Channel) bool {
if !client.flags[Operator] && channel.flags[Private] {
if !CanSeeChannel(client, channel) {
return true
}
client.RplList(channel)
return false
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
}
@@ -624,7 +864,7 @@ func (msg *ListCommand) HandleServer(server *Server) {
func (msg *NamesCommand) HandleServer(server *Server) {
client := msg.Client()
if server.channels.Length() == 0 {
if server.channels.Count() == 0 {
server.channels.Range(func(name Name, channel *Channel) bool {
channel.Names(client)
return true
@@ -690,6 +930,16 @@ func (msg *LUsersCommand) HandleServer(server *Server) {
client.RplLUserMe()
}
func (msg *WallopsCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.flags[Operator] {
client.ErrNoPrivileges()
return
}
server.Wallops(msg.message.String())
}
func (msg *KillCommand) HandleServer(server *Server) {
client := msg.Client()
if !client.flags[Operator] {

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"io"
"net"
"sync"
log "github.com/sirupsen/logrus"
)
@@ -14,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 {
@@ -33,6 +35,9 @@ func (socket *Socket) String() string {
}
func (socket *Socket) Close() {
socket.closedMutex.Lock()
defer socket.closedMutex.Unlock()
if socket.closed {
return
}
@@ -42,6 +47,8 @@ func (socket *Socket) Close() {
}
func (socket *Socket) Read() (line string, err error) {
socket.closedMutex.RLock()
defer socket.closedMutex.RUnlock()
if socket.closed {
err = io.EOF
return
@@ -65,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

View File

@@ -10,6 +10,29 @@ import (
// simple types
//
type Counter struct {
sync.RWMutex
value int
}
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 {
@@ -24,8 +47,8 @@ func NewChannelNameMap() *ChannelNameMap {
}
}
// Length returns the number of *Channel9s)
func (c *ChannelNameMap) Length() int {
// Count returns the number of *Channel9s)
func (c *ChannelNameMap) Count() int {
c.RLock()
defer c.RUnlock()
return len(c.channels)
@@ -37,7 +60,7 @@ func (c *ChannelNameMap) Range(f func(kay Name, value *Channel) bool) {
defer c.Unlock()
for k, v := range c.channels {
if !f(k, v) {
break
return
}
}
}
@@ -71,82 +94,239 @@ func (c *ChannelNameMap) Remove(channel *Channel) error {
return nil
}
type ChannelModeSet map[ChannelMode]bool
// ChannelModeSet holds a mapping of channel modes
type ChannelModeSet struct {
sync.RWMutex
modes map[ChannelMode]bool
}
func (set ChannelModeSet) String() string {
if len(set) == 0 {
// NewChannelModeSet returns a new ChannelModeSet
func NewChannelModeSet() *ChannelModeSet {
return &ChannelModeSet{modes: make(map[ChannelMode]bool)}
}
// Set sets mode
func (set *ChannelModeSet) Set(mode ChannelMode) {
set.Lock()
defer set.Unlock()
set.modes[mode] = true
}
// Unset unsets mode
func (set *ChannelModeSet) Unset(mode ChannelMode) {
set.Lock()
defer set.Unlock()
delete(set.modes, mode)
}
// 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
}
// 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))
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
type ClientSet struct {
sync.RWMutex
clients map[*Client]bool
}
func (clients ClientSet) Remove(client *Client) {
delete(clients, client)
func NewClientSet() *ClientSet {
return &ClientSet{clients: make(map[*Client]bool)}
}
func (clients ClientSet) Has(client *Client) bool {
return clients[client]
func (set *ClientSet) Add(client *Client) {
set.Lock()
defer set.Unlock()
set.clients[client] = true
}
type MemberSet map[*Client]ChannelModeSet
func (members MemberSet) Add(member *Client) {
members[member] = make(ChannelModeSet)
func (set *ClientSet) Remove(client *Client) {
set.Lock()
defer set.Unlock()
delete(set.clients, client)
}
func (members MemberSet) Remove(member *Client) {
delete(members, member)
func (set *ClientSet) Count() int {
set.RLock()
defer set.RUnlock()
return len(set.clients)
}
func (members MemberSet) Has(member *Client) bool {
_, ok := members[member]
func (set *ClientSet) Has(client *Client) bool {
set.RLock()
defer set.RUnlock()
ok, _ := set.clients[client]
return ok
}
func (members MemberSet) HasMode(member *Client, mode ChannelMode) bool {
modes, ok := members[member]
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,18 @@
package irc
import (
"fmt"
)
var (
//PackageName package name
Package = "eris"
// Version release version
Version = "1.5.4"
Version = "1.6.4"
// Build will be overwritten automatically by the build system
Build = "-dev"
Build = "dev"
// GitCommit will be overwritten automatically by the build system
GitCommit = "HEAD"
@@ -13,5 +20,5 @@ var (
// FullVersion display the full version and build
func FullVersion() string {
return Version + Build + " (" + GitCommit + ")"
return fmt.Sprintf("%s-%s-%s@%s", Package, Version, Build, 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,3 +1,7 @@
network:
# network name
name: Local
server:
# server name
name: localhost.localdomain
@@ -16,7 +20,7 @@ server:
cert: cert.pem
# password to login to the server
# generated using "ircd genpasswd"
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
#password: ""
# motd filename
@@ -27,5 +31,12 @@ operator:
# operator named 'admin' with password 'password'
admin:
# password to login with /OPER command
# generated using "ircd genpasswd"
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
# accounts (SASL)
account:
# username 'admin'
admin:
# password 'admin'
password: JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD

61
main.go
View File

@@ -1,63 +1,42 @@
package main
import (
"flag"
"fmt"
"syscall"
"os"
"github.com/docopt/docopt-go"
"github.com/prologic/eris/irc"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
"github.com/prologic/eris/irc"
)
func main() {
version := irc.FullVersion()
usage := `eris.
Usage:
eris genpasswd [--conf <filename>]
eris run [--conf <filename>] [ -d | --debug ]
eris -h | --help
eris -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()
if arguments["-d"].(bool) || arguments["--debug"].(bool) {
if version {
fmt.Printf(irc.FullVersion())
os.Exit(0)
}
if debug {
log.SetLevel(log.DebugLevel)
} else {
log.SetLevel(log.WarnLevel)
}
// 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
}
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) {
server := irc.NewServer(config)
log.Println(irc.FullVersion(), "running")
defer log.Println(irc.FullVersion(), "exiting")
server.Run()
}
irc.NewServer(config).Run()
}

529
main_test.go Normal file
View File

@@ -0,0 +1,529 @@
package main
import (
"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)
var (
expected bool
actual chan bool
)
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)
var (
expected string
actual chan string
)
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)
var (
expected []string
actual chan string
)
actual = make(chan string)
client := newClient(true)
client.AddCallback("001", func(e *irc.Event) {
expected = []string{e.Arguments[0], "=", "#join", fmt.Sprintf("@%s", e.Arguments[0])}
})
client.AddCallback("353", func(e *irc.Event) {
for i := range e.Arguments {
actual <- e.Arguments[i]
}
})
defer client.Quit()
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)
var (
expected bool
actual chan bool
)
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], "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()
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_PRIVMSG(t *testing.T) {
assert := assert.New(t)
var (
expected string
actual chan string
)
expected = "Hello World!"
actual = make(chan string)
client1 := newClient(true)
client2 := newClient(true)
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()
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)
var (
expected string
actual chan string
)
expected = "Hello World!"
actual = make(chan string)
client1 := newClient(true)
client2 := newClient(true)
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()
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)
var (
expected bool
actual chan bool
)
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()
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(true)
defer client1.Quit()
client1.AddCallback("403", func(e *irc.Event) {
actual <- true
})
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(true)
client2 := newClient(true)
defer client1.Quit()
defer client2.Quit()
client1.AddCallback("442", func(e *irc.Event) {
actual <- true
})
client2.AddCallback("JOIN", func(e *irc.Event) {
client1.SendRaw("TOPIC #notonchannel :FooBar")
})
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)
var (
expected bool
actual chan bool
)
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.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()
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)
var (
expected bool
actual chan bool
)
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()
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"

1
vendor/github.com/beorn7/perks generated vendored Submodule

1
vendor/github.com/golang/protobuf generated vendored Submodule

1
vendor/github.com/prometheus/common generated vendored Submodule

1
vendor/github.com/prometheus/procfs generated vendored Submodule

1
vendor/github.com/satori/go.uuid generated vendored Submodule

1
vendor/github.com/stretchr/testify generated vendored Submodule

1
vendor/github.com/thoj/go-ircevent generated vendored Submodule