Compare commits
70 Commits
v1.5.4
...
test_cover
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0e07999cb2 | ||
![]() |
79c60eafaf | ||
![]() |
0ec9aaa5eb | ||
![]() |
90910a20b1 | ||
![]() |
43e4726461 | ||
![]() |
493b58459d | ||
![]() |
d53b08b3a0 | ||
![]() |
8ba7a2c1e2 | ||
![]() |
aed917ea2f | ||
![]() |
94bbfe4baf | ||
![]() |
8732f70630 | ||
![]() |
b7f798d600 | ||
![]() |
caab002d51 | ||
![]() |
7ff892bba9 | ||
![]() |
233238b709 | ||
![]() |
59e0792db1 | ||
![]() |
962b6645c1 | ||
![]() |
cee8bf9957 | ||
![]() |
9d93bca179 | ||
![]() |
ccae795335 | ||
![]() |
862eb429d4 | ||
![]() |
9e075dde67 | ||
![]() |
20be29bcef | ||
![]() |
34c3be0a88 | ||
![]() |
be246a3bc4 | ||
![]() |
4fb452b2c0 | ||
![]() |
d707382a78 | ||
![]() |
7620a3c282 | ||
![]() |
18a3e2f2c3 | ||
![]() |
d046a9863f | ||
![]() |
a1450a81d6 | ||
![]() |
d594386658 | ||
![]() |
89b512fc76 | ||
![]() |
d01bb4fe57 | ||
![]() |
2fef0feb5a | ||
![]() |
735458ffed | ||
![]() |
02427bcb3f | ||
![]() |
bdcb4c21a5 | ||
![]() |
0e3be3f34c | ||
![]() |
19e564ed2b | ||
![]() |
ef10282a37 | ||
![]() |
3a9d1fefc8 | ||
![]() |
f5d8f22220 | ||
![]() |
062e2546ab | ||
![]() |
8f269b5201 | ||
![]() |
d33d60353c | ||
![]() |
9a5862287b | ||
![]() |
46d22a71b3 | ||
![]() |
4d97e035d2 | ||
![]() |
41b6511cec | ||
![]() |
1cde7c6902 | ||
![]() |
edfd990d59 | ||
![]() |
768f4f215a | ||
![]() |
9601098872 | ||
![]() |
d97fc927ad | ||
![]() |
28ed5cc2c0 | ||
![]() |
4ff06efab8 | ||
![]() |
51e1a93a99 | ||
![]() |
db4a9a864e | ||
![]() |
e333eb6029 | ||
![]() |
700c242e35 | ||
![]() |
c2512ca082 | ||
![]() |
7e41395abd | ||
![]() |
c1110f8b81 | ||
![]() |
87663a4175 | ||
![]() |
988820efb3 | ||
![]() |
91212c3254 | ||
![]() |
02b3525ef7 | ||
![]() |
12d562c0fa | ||
![]() |
ec084f49ab |
14
.drone.yml
14
.drone.yml
@@ -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
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
*~*
|
||||
bin
|
||||
*.db
|
||||
*.pem
|
||||
|
||||
|
36
.gitmodules
vendored
36
.gitmodules
vendored
@@ -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
8
.travis.yml
Normal 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
49
CONTRIBUTING.md
Normal 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.
|
@@ -27,4 +27,4 @@ COPY --from=build /go/src/github.com/${REPO}/${APP} /${APP}
|
||||
EXPOSE 6667/tcp 6697/tcp
|
||||
|
||||
ENTRYPOINT ["/eris"]
|
||||
CMD ["run"]
|
||||
CMD [""]
|
||||
|
2
Makefile
2
Makefile
@@ -6,7 +6,7 @@ APP=eris
|
||||
PACKAGE=irc
|
||||
REPO?=prologic/$(APP)
|
||||
TAG?=latest
|
||||
BUILD?=-dev
|
||||
BUILD?=dev
|
||||
|
||||
all: dev
|
||||
|
||||
|
94
README.md
94
README.md
@@ -1,5 +1,11 @@
|
||||
# eris - IRC Server / Daemon written in Go
|
||||
|
||||
[](https://travis-ci.org/prologic/eris)
|
||||
[](https://goreportcard.com/report/github.com/prologic/eris)
|
||||
[](https://coveralls.io/r/prologic/eris)
|
||||
[](https://godoc.org/github.com/prologic/eris)
|
||||
[](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
38
docker-compose.yml
Normal 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
|
@@ -27,6 +27,7 @@ const (
|
||||
var (
|
||||
SupportedCapabilities = CapabilitySet{
|
||||
MultiPrefix: true,
|
||||
SASL: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
130
irc/channel.go
130
irc/channel.go
@@ -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)
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
194
irc/metrics.go
Normal 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))
|
||||
}
|
26
irc/modes.go
26
irc/modes.go
@@ -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
|
||||
|
121
irc/password.go
121
irc/password.go
@@ -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
22
irc/privacy.go
Normal 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
|
||||
}
|
211
irc/reply.go
211
irc/reply.go
@@ -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
84
irc/sasl.go
Normal 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
|
||||
}
|
330
irc/server.go
330
irc/server.go
@@ -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] {
|
||||
|
@@ -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
|
||||
|
268
irc/types.go
268
irc/types.go
@@ -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
11
irc/utils.go
Normal 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)
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
15
ircd.yml
15
ircd.yml
@@ -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
61
main.go
@@ -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
529
main_test.go
Normal 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
41
scripts/release.sh
Executable 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/beorn7/perks
generated
vendored
Submodule
Submodule vendor/github.com/beorn7/perks added at 4c0e84591b
1
vendor/github.com/docopt/docopt-go
generated
vendored
1
vendor/github.com/docopt/docopt-go
generated
vendored
Submodule vendor/github.com/docopt/docopt-go deleted from 784ddc5885
1
vendor/github.com/golang/protobuf
generated
vendored
Submodule
1
vendor/github.com/golang/protobuf
generated
vendored
Submodule
Submodule vendor/github.com/golang/protobuf added at 1e59b77b52
1
vendor/github.com/matttproud/golang_protobuf_extensions
generated
vendored
Submodule
1
vendor/github.com/matttproud/golang_protobuf_extensions
generated
vendored
Submodule
Submodule vendor/github.com/matttproud/golang_protobuf_extensions added at c12348ce28
1
vendor/github.com/prometheus/client_golang
generated
vendored
Submodule
1
vendor/github.com/prometheus/client_golang
generated
vendored
Submodule
Submodule vendor/github.com/prometheus/client_golang added at 661e31bf84
1
vendor/github.com/prometheus/client_model
generated
vendored
Submodule
1
vendor/github.com/prometheus/client_model
generated
vendored
Submodule
Submodule vendor/github.com/prometheus/client_model added at 99fa1f4be8
1
vendor/github.com/prometheus/common
generated
vendored
Submodule
1
vendor/github.com/prometheus/common
generated
vendored
Submodule
Submodule vendor/github.com/prometheus/common added at 2e54d0b93c
1
vendor/github.com/prometheus/procfs
generated
vendored
Submodule
1
vendor/github.com/prometheus/procfs
generated
vendored
Submodule
Submodule vendor/github.com/prometheus/procfs added at a6e9df898b
1
vendor/github.com/renstrom/shortuuid
generated
vendored
Submodule
1
vendor/github.com/renstrom/shortuuid
generated
vendored
Submodule
Submodule vendor/github.com/renstrom/shortuuid added at d728e00b72
1
vendor/github.com/satori/go.uuid
generated
vendored
Submodule
1
vendor/github.com/satori/go.uuid
generated
vendored
Submodule
Submodule vendor/github.com/satori/go.uuid added at 5bf94b69c6
2
vendor/github.com/sirupsen/logrus
generated
vendored
2
vendor/github.com/sirupsen/logrus
generated
vendored
Submodule vendor/github.com/sirupsen/logrus updated: 89742aefa4...95cd2b9c79
1
vendor/github.com/stretchr/testify
generated
vendored
Submodule
1
vendor/github.com/stretchr/testify
generated
vendored
Submodule
Submodule vendor/github.com/stretchr/testify added at 2aa2c176b9
1
vendor/github.com/thoj/go-ircevent
generated
vendored
Submodule
1
vendor/github.com/thoj/go-ircevent
generated
vendored
Submodule
Submodule vendor/github.com/thoj/go-ircevent added at db5bd176f7
2
vendor/golang.org/x/crypto
generated
vendored
2
vendor/golang.org/x/crypto
generated
vendored
Submodule vendor/golang.org/x/crypto updated: 9f005a07e0...94eea52f7b
2
vendor/golang.org/x/sys
generated
vendored
2
vendor/golang.org/x/sys
generated
vendored
Submodule vendor/golang.org/x/sys updated: bf42f188b9...8b4580aae2
2
vendor/golang.org/x/text
generated
vendored
2
vendor/golang.org/x/text
generated
vendored
Submodule vendor/golang.org/x/text updated: 88f656faf3...556d234e9c
2
vendor/gopkg.in/yaml.v2
generated
vendored
2
vendor/gopkg.in/yaml.v2
generated
vendored
Submodule vendor/gopkg.in/yaml.v2 updated: eb3733d160...287cf08546
Reference in New Issue
Block a user