Compare commits
83 Commits
v1.5.2
...
fix_bad_la
Author | SHA1 | Date | |
---|---|---|---|
![]() |
088333054c | ||
![]() |
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 | ||
![]() |
97d5a1e199 | ||
![]() |
9e4115672c | ||
![]() |
af9207438a | ||
![]() |
4f9195e3e1 | ||
![]() |
5e94c932ff | ||
![]() |
6195810cd6 | ||
![]() |
cc58f7ff62 | ||
![]() |
af8d3161f3 | ||
![]() |
0027d88e68 | ||
![]() |
59b3cb1d7c | ||
![]() |
5e945c863f | ||
![]() |
564f774a93 | ||
![]() |
574a486ecd | ||
![]() |
8b52af0f17 | ||
![]() |
29082554b7 | ||
![]() |
59f21700d6 | ||
![]() |
6622b514c4 | ||
![]() |
e989b0111a | ||
![]() |
78a7f3dde6 | ||
![]() |
798e9f3cd2 | ||
![]() |
9269ce4d52 | ||
![]() |
500d14dafc | ||
![]() |
cbbfd995f6 | ||
![]() |
1c24352a4f | ||
![]() |
e26b8bb980 | ||
![]() |
4a95857377 | ||
![]() |
83366c54f6 | ||
![]() |
de55aeff29 |
@@ -1,6 +1,6 @@
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/prologic/ircd
|
||||
path: src/github.com/prologic/eris
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
@@ -11,7 +11,7 @@ pipeline:
|
||||
|
||||
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 +21,7 @@ pipeline:
|
||||
from: drone@mills.io
|
||||
skip_verify: true
|
||||
when:
|
||||
status: [ changed, failure ]
|
||||
status: [ success, changed, failure ]
|
||||
|
||||
secrets:
|
||||
registry_username:
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
*~*
|
||||
bin
|
||||
*.db
|
||||
ircd
|
||||
*.pem
|
||||
|
||||
eris
|
||||
|
51
.gitmodules
vendored
Normal file
51
.gitmodules
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
[submodule "vendor/github.com/sirupsen/logrus"]
|
||||
path = vendor/github.com/sirupsen/logrus
|
||||
url = https://github.com/sirupsen/logrus
|
||||
[submodule "vendor/golang.org/x/crypto"]
|
||||
path = vendor/golang.org/x/crypto
|
||||
url = https://go.googlesource.com/crypto
|
||||
[submodule "vendor/golang.org/x/sys"]
|
||||
path = vendor/golang.org/x/sys
|
||||
url = https://go.googlesource.com/sys
|
||||
[submodule "vendor/github.com/DanielOaks/girc-go"]
|
||||
path = vendor/github.com/DanielOaks/girc-go
|
||||
url = https://github.com/DanielOaks/girc-go
|
||||
[submodule "vendor/github.com/goshuirc/e-nfa"]
|
||||
path = vendor/github.com/goshuirc/e-nfa
|
||||
url = https://github.com/goshuirc/e-nfa
|
||||
[submodule "vendor/github.com/imdario/mergo"]
|
||||
path = vendor/github.com/imdario/mergo
|
||||
url = https://github.com/imdario/mergo
|
||||
[submodule "vendor/golang.org/x/text"]
|
||||
path = vendor/golang.org/x/text
|
||||
url = https://go.googlesource.com/text
|
||||
[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/sasha-s/go-deadlock"]
|
||||
path = vendor/github.com/sasha-s/go-deadlock
|
||||
url = https://github.com/sasha-s/go-deadlock
|
||||
[submodule "vendor/github.com/petermattis/goid"]
|
||||
path = vendor/github.com/petermattis/goid
|
||||
url = https://github.com/petermattis/goid
|
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.
|
10
Dockerfile
10
Dockerfile
@@ -4,7 +4,7 @@ FROM golang:alpine AS build
|
||||
ARG TAG
|
||||
ARG BUILD
|
||||
|
||||
ENV APP ircd
|
||||
ENV APP eris
|
||||
ENV REPO prologic/$APP
|
||||
|
||||
RUN apk add --update git make build-base && \
|
||||
@@ -17,14 +17,14 @@ RUN make TAG=$TAG BUILD=$BUILD build
|
||||
# Runtime
|
||||
FROM alpine
|
||||
|
||||
ENV APP ircd
|
||||
ENV APP eris
|
||||
ENV REPO prologic/$APP
|
||||
|
||||
LABEL ircd.app main
|
||||
LABEL eris.app main
|
||||
|
||||
COPY --from=build /go/src/github.com/${REPO}/${APP} /${APP}
|
||||
|
||||
EXPOSE 6667/tcp 6697/tcp
|
||||
|
||||
ENTRYPOINT ["/ircd"]
|
||||
CMD ["run"]
|
||||
ENTRYPOINT ["/eris"]
|
||||
CMD [""]
|
||||
|
37
LICENSE
37
LICENSE
@@ -1,22 +1,23 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (C) 2017 James Mills
|
||||
Copyright (C) 2014 Jeremy Latt
|
||||
|
||||
Copyright (c) 2017 James Mills
|
||||
Copyright (c) 2014 Jeremy Latt
|
||||
eris is covered by the MIT license::
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
||||
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
7
Makefile
7
Makefile
@@ -2,10 +2,11 @@
|
||||
|
||||
CGO_ENABLED=0
|
||||
COMMIT=`git rev-parse --short HEAD`
|
||||
APP=ircd
|
||||
APP=eris
|
||||
PACKAGE=irc
|
||||
REPO?=prologic/$(APP)
|
||||
TAG?=latest
|
||||
BUILD?=-dev
|
||||
BUILD?=dev
|
||||
|
||||
all: dev
|
||||
|
||||
@@ -18,7 +19,7 @@ deps:
|
||||
build: clean deps
|
||||
@echo " -> Building $(TAG)$(BUILD)"
|
||||
@go build -tags "netgo static_build" -installsuffix netgo \
|
||||
-ldflags "-w -X github.com/$(REPO)/$(APP).GitCommit=$(COMMIT) -X github.com/$(REPO)/$(APP).Build=$(BUILD)" .
|
||||
-ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT) -X github.com/$(REPO)/${PACKAGE}.Build=$(BUILD)" .
|
||||
@echo "Built $$(./$(APP) -v)"
|
||||
|
||||
image:
|
||||
|
125
README.md
125
README.md
@@ -1,4 +1,10 @@
|
||||
# ircd - IRC Daemon
|
||||
# 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)
|
||||
@@ -8,12 +14,31 @@
|
||||
|
||||
----
|
||||
|
||||
ircd is an IRC daemon written from scratch in Go.
|
||||
> In philosophy and rhetoric, eristic (from Eris, the ancient Greek goddess
|
||||
> of chaos, strife, and discord) refers to argument that aims to successfully
|
||||
> dispute another's argument, rather than searching for truth. According to T.H.
|
||||
|
||||
From [Eris](https://en.wikipedia.org/wiki/Eris_(mythology))
|
||||
and [Eristic](https://en.wikipedia.org/wiki/Eristic)
|
||||
|
||||
The connotation here is that IRC (*Internet Relay Chat*) is a place of chaos,
|
||||
strife and discord. IRC is a place where you argue and get into arguments for
|
||||
the sake of argument.
|
||||
|
||||
So `eris` is an IRC daemon written from scratch in Go to factiliate discord
|
||||
and have arguments for the sake of argument!
|
||||
|
||||
Pull requests and issues are welcome.
|
||||
|
||||
Discussion at:
|
||||
* host/port: irc.mills.io:6697 (*use SSL*)
|
||||
* #lobby
|
||||
|
||||
* /server irc.mills.io +6697 (*use TLS/SSL*)
|
||||
* /join #lobby
|
||||
|
||||
Or (**not recommended**):
|
||||
|
||||
* /server irc.mills.io (*default port 6667, non-TLS*)
|
||||
* /join #lobby
|
||||
|
||||
## Features
|
||||
|
||||
@@ -28,32 +53,102 @@ Discussion at:
|
||||
* SSL/TLS support
|
||||
* Simple IRC operator privileges (*overrides most things*)
|
||||
* Secure connection tracking (+z) and SecureOnly user mode (+Z)
|
||||
* Secure channels (+Z)
|
||||
* Three layers of channel privacy, Public, Private (+p) and Secret (s)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```#!bash
|
||||
$ go get github.com/prologic/eris
|
||||
$ cat > ircd.yml <<EOF
|
||||
network:
|
||||
name: Test
|
||||
server:
|
||||
name: Test
|
||||
listen:
|
||||
- ":6667"
|
||||
EOF
|
||||
$ eris
|
||||
```
|
||||
|
||||
If you want TLS (**recommended**) then:
|
||||
|
||||
```#!bash
|
||||
$ go get github.com/prologic/mksslcert
|
||||
$ mksslcert
|
||||
```
|
||||
|
||||
This generates a self-signed cert `cert.pem` and `key.pem` into the `$PWD`.
|
||||
|
||||
Then add a `tlslisten` block to your config:
|
||||
|
||||
```#!yaml
|
||||
server:
|
||||
tlslisten:
|
||||
":6697":
|
||||
key: key.pem
|
||||
cert: cert.pem
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```#!bash
|
||||
$ go install github.com/prologic/ircd
|
||||
$ ircd --help
|
||||
$ go install github.com/prologic/eris
|
||||
$ eris --help
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See the example [ircd.yml](ircd.yml). Passwords are base64-encoded
|
||||
bcrypted byte strings. You can generate them with the `genpasswd` subcommand.
|
||||
bcrypted byte strings. You can generate them with the `mkpasswd` tool
|
||||
from [prologic/mkpasswd](https://github.com/prologic/mkpasswd):
|
||||
|
||||
```#!bash
|
||||
$ ircd genpasswd
|
||||
$ go install github.com/prologic/mkpasswd
|
||||
$ mkpasswd
|
||||
```
|
||||
|
||||
## Running the server
|
||||
Self-signed certificates can also be generated using the `mksslcert` tool
|
||||
from [prologic/mksslcert](https://github.com/prologic/mksslcert):
|
||||
|
||||
```#!bash
|
||||
$ ircd run
|
||||
$ go install github.com/prologic/mksslcert
|
||||
$ mksslcert
|
||||
```
|
||||
|
||||
## Credits
|
||||
## Deployment
|
||||
|
||||
* Jeremy Latt, creator, <https://github.com/jlatt>
|
||||
* Edmund Huber, maintainer, <https://github.com/edmund-huber>
|
||||
* Niels Freier, added WebSocket support, <https://github.com/stumpyfr>
|
||||
* apologies to anyone I forgot.
|
||||
To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*):
|
||||
|
||||
```#!bash
|
||||
$ eris
|
||||
```
|
||||
|
||||
Or you can deploy with [Docker](https://www.docker.com) using the prebuilt [prologic/eris](https://hub.docker.com/r/prologic/eris/):
|
||||
|
||||
```#!bash
|
||||
docker run -d -p 6667:6667 -p 6697:6697 prologic/eris
|
||||
```
|
||||
|
||||
You may want to customize the configuration however and create your own image based off of this; or deploy with `docker stack deploy` on a [Docker Swarm](https://docs.docker.com/engine/swarm/) clsuter like this:
|
||||
|
||||
```#!bash
|
||||
$ docker stack deploy -c docker-compose.yml eris
|
||||
```
|
||||
|
||||
Which assumes a `ircd.yml` coniguration file in the current directory which Docker will use to distribute as the configuration. The `docker-compose.yml` (*Docker Stackfile*) is available at the root of this repository.
|
||||
|
||||
## Related Projects
|
||||
|
||||
There are a number of supported accompanying services that are being developed alongside Eris:
|
||||
|
||||
* [Soter](https://github.com/prologic/soter) -- An IRC Bot that persists channel modes and topics.
|
||||
* [Cadmus](https://github.com/prologic/cadmus) -- An IRC Bot that logs channels and provides an interface for viewing and searching logs
|
||||
|
||||
## Recommended 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.
|
||||
|
18
cert.pem
18
cert.pem
@@ -1,18 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+TCCAeGgAwIBAgIQf965BX8kslTRGmssCj1PlDANBgkqhkiG9w0BAQsFADAS
|
||||
MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTEwNDIxNTQyMloXDTE4MTEwNDIxNTQy
|
||||
MlowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBAKsF2aoSh7Ufb0YCJG+tvJCGZhPecK+OiVbF/wi7eX66aJIj1eLm9v3V
|
||||
0068lNNZDql0ngTb0dI1IoOscD2YIVdoFidtBOfT8vPjtTzLh7G4XquVML8tG69i
|
||||
7VutHvQtN/6kLFv+mZB/bHZuQsa1qvTqVjbQnJ5p/UeGd0dN9bYgaNdXsu0C3YbU
|
||||
683XhpkdwU8tdLlOPNRV3yRZO8L47buhRFYMhQ8y0U9bKHi3akEFzRQphIYoM8kj
|
||||
RyvvLIfOKFyVj/WUkkpsQWSATuDBEzp84UG+jOoucRr2ck9bMGBYohyvu6ezd8IR
|
||||
JcjAiQ1pabSetldeTCuavXSnjr8grtkCAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWg
|
||||
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ
|
||||
bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQByPoh1182g8+jRd4mqDk6n0/Zx
|
||||
PSBrhAQd8IID/P3jj8mn8Q8rgktmnEGYSvRVCpsyswiZHW45xvAHjV1msc6tIHSL
|
||||
Lpkgf1E8qrQ6Cam72/W/dhiKSrVs50K2SGEHY8Ij8d0Q/KPkQqdAC7hbHxD4vbaA
|
||||
iUAqEZ6d2s9sxEnZvYlol6ITDcQ8kOozSJN51YU5GDT8Vc/qkqRJ3OOn4vss22V2
|
||||
upXZf33Ci6Dm4xUBd4F0x3dri+OglyLnjWz2sn+OSb/+apS08GiycBqPeYW+KjdA
|
||||
YHPBnRvjQ35M5FL9cM9tQhcwQ9dPIUy80qehUQjiNOBgh6SD8EGBemLrDEPa
|
||||
-----END CERTIFICATE-----
|
38
docker-compose.yml
Normal file
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,
|
||||
}
|
||||
)
|
||||
|
||||
|
139
irc/channel.go
139
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
|
||||
@@ -17,26 +17,32 @@ type Channel struct {
|
||||
|
||||
// NewChannel creates a new channel from a `Server` and a `name`
|
||||
// string, which must be unique on the server.
|
||||
func NewChannel(s *Server, name Name) *Channel {
|
||||
func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
|
||||
channel := &Channel{
|
||||
flags: make(ChannelModeSet),
|
||||
flags: NewChannelModeSet(),
|
||||
lists: map[ChannelMode]*UserMaskSet{
|
||||
BanMask: NewUserMaskSet(),
|
||||
ExceptMask: NewUserMaskSet(),
|
||||
InviteMask: NewUserMaskSet(),
|
||||
},
|
||||
members: make(MemberSet),
|
||||
members: NewMemberSet(),
|
||||
name: name,
|
||||
server: s,
|
||||
}
|
||||
|
||||
if addDefaultModes {
|
||||
for _, mode := range DefaultChannelModes {
|
||||
channel.flags.Set(mode)
|
||||
}
|
||||
}
|
||||
|
||||
s.channels.Add(channel)
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
func (channel *Channel) IsEmpty() bool {
|
||||
return len(channel.members) == 0
|
||||
return channel.members.Count() == 0
|
||||
}
|
||||
|
||||
func (channel *Channel) Names(client *Client) {
|
||||
@@ -50,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
|
||||
}
|
||||
|
||||
@@ -100,9 +109,10 @@ func (channel *Channel) ModeString(client *Client) (str string) {
|
||||
}
|
||||
|
||||
// flags
|
||||
for mode := range channel.flags {
|
||||
channel.flags.Range(func(mode ChannelMode) bool {
|
||||
str += mode.String()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
str = "+" + str
|
||||
|
||||
@@ -120,7 +130,7 @@ func (channel *Channel) ModeString(client *Client) (str string) {
|
||||
|
||||
func (channel *Channel) IsFull() bool {
|
||||
return (channel.userLimit > 0) &&
|
||||
(uint64(len(channel.members)) >= channel.userLimit)
|
||||
(uint64(channel.members.Count()) >= channel.userLimit)
|
||||
}
|
||||
|
||||
func (channel *Channel) CheckKey(key Text) bool {
|
||||
@@ -145,31 +155,32 @@ func (channel *Channel) Join(client *Client, key Text) {
|
||||
return
|
||||
}
|
||||
|
||||
isInvited := channel.lists[InviteMask].Match(client.UserHost())
|
||||
if !isOperator && channel.flags[InviteOnly] && !isInvited {
|
||||
isInvited := channel.lists[InviteMask].Match(client.UserHost(false))
|
||||
if !isOperator && channel.flags.Has(InviteOnly) && !isInvited {
|
||||
client.ErrInviteOnlyChan(channel)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.lists[BanMask].Match(client.UserHost()) &&
|
||||
if channel.lists[BanMask].Match(client.UserHost(false)) &&
|
||||
!isInvited &&
|
||||
!isOperator &&
|
||||
!channel.lists[ExceptMask].Match(client.UserHost()) {
|
||||
!channel.lists[ExceptMask].Match(client.UserHost(false)) {
|
||||
client.ErrBannedFromChan(channel)
|
||||
return
|
||||
}
|
||||
|
||||
client.channels.Add(channel)
|
||||
channel.members.Add(client)
|
||||
if len(channel.members) == 1 {
|
||||
channel.members[client][ChannelCreator] = true
|
||||
channel.members[client][ChannelOperator] = true
|
||||
if channel.members.Count() == 1 {
|
||||
channel.members.Get(client).Set(ChannelCreator)
|
||||
channel.members.Get(client).Set(ChannelOperator)
|
||||
}
|
||||
|
||||
reply := RplJoin(client, channel)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.GetTopic(client)
|
||||
channel.Names(client)
|
||||
}
|
||||
@@ -181,9 +192,10 @@ func (channel *Channel) Part(client *Client, message Text) {
|
||||
}
|
||||
|
||||
reply := RplPart(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.Quit(client)
|
||||
}
|
||||
|
||||
@@ -194,8 +206,7 @@ func (channel *Channel) GetTopic(client *Client) {
|
||||
}
|
||||
|
||||
if channel.topic == "" {
|
||||
// clients appear not to expect this
|
||||
//replier.Reply(RplNoTopic(channel))
|
||||
client.RplNoTopic(channel)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,7 +219,7 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
|
||||
return
|
||||
}
|
||||
|
||||
if channel.flags[OpOnlyTopic] && !channel.ClientIsOperator(client) {
|
||||
if channel.flags.Has(OpOnlyTopic) && !channel.ClientIsOperator(client) {
|
||||
client.ErrChanOPrivIsNeeded(channel)
|
||||
return
|
||||
}
|
||||
@@ -216,22 +227,26 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
|
||||
channel.topic = topic
|
||||
|
||||
reply := RplTopicMsg(client, channel)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) CanSpeak(client *Client) bool {
|
||||
if channel.ClientIsOperator(client) {
|
||||
return true
|
||||
}
|
||||
if channel.flags[NoOutside] && !channel.members.Has(client) {
|
||||
if channel.flags.Has(NoOutside) && !channel.members.Has(client) {
|
||||
return false
|
||||
}
|
||||
if channel.flags[Moderated] && !(channel.members.HasMode(client, Voice) ||
|
||||
if channel.flags.Has(Moderated) && !(channel.members.HasMode(client, Voice) ||
|
||||
channel.members.HasMode(client, ChannelOperator)) {
|
||||
return false
|
||||
}
|
||||
if channel.flags.Has(SecureChan) && !client.flags[SecureConn] {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -241,12 +256,14 @@ func (channel *Channel) PrivMsg(client *Client, message Text) {
|
||||
return
|
||||
}
|
||||
reply := RplPrivMsg(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
if member == client {
|
||||
continue
|
||||
return true
|
||||
}
|
||||
client.server.metrics.Counter("client", "messages").Inc()
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
|
||||
@@ -258,17 +275,17 @@ func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
|
||||
|
||||
switch op {
|
||||
case Add:
|
||||
if channel.flags[mode] {
|
||||
if channel.flags.Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.flags[mode] = true
|
||||
channel.flags.Set(mode)
|
||||
return true
|
||||
|
||||
case Remove:
|
||||
if !channel.flags[mode] {
|
||||
if !channel.flags.Has(mode) {
|
||||
return false
|
||||
}
|
||||
delete(channel.flags, mode)
|
||||
channel.flags.Unset(mode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -299,17 +316,17 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode,
|
||||
|
||||
switch op {
|
||||
case Add:
|
||||
if channel.members[target][mode] {
|
||||
if channel.members.Get(target).Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.members[target][mode] = true
|
||||
channel.members.Get(target).Set(mode)
|
||||
return true
|
||||
|
||||
case Remove:
|
||||
if !channel.members[target][mode] {
|
||||
if !channel.members.Get(target).Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.members[target][mode] = false
|
||||
channel.members.Get(target).Unset(mode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -357,7 +374,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo
|
||||
return channel.applyModeMask(client, change.mode, change.op,
|
||||
NewName(change.arg))
|
||||
|
||||
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private:
|
||||
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private, Secret, SecureChan:
|
||||
return channel.applyModeFlag(client, change.mode, change.op)
|
||||
|
||||
case Key:
|
||||
@@ -423,9 +440,10 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) {
|
||||
|
||||
if len(applied) > 0 {
|
||||
reply := RplChannelMode(client, channel, applied)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,17 +453,21 @@ func (channel *Channel) Notice(client *Client, message Text) {
|
||||
return
|
||||
}
|
||||
reply := RplNotice(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
if member == client {
|
||||
continue
|
||||
return true
|
||||
}
|
||||
client.server.metrics.Counter("client", "messages").Inc()
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) Quit(client *Client) {
|
||||
channel.members.Remove(client)
|
||||
client.channels.Remove(channel)
|
||||
// XXX: Race Condition from client.destroy()
|
||||
// Do we need to?
|
||||
// client.channels.Remove(channel)
|
||||
|
||||
if channel.IsEmpty() {
|
||||
channel.server.channels.Remove(channel)
|
||||
@@ -467,14 +489,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment Text) {
|
||||
}
|
||||
|
||||
reply := RplKick(channel, client, target, comment)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.Quit(target)
|
||||
}
|
||||
|
||||
func (channel *Channel) Invite(invitee *Client, inviter *Client) {
|
||||
if channel.flags[InviteOnly] && !channel.ClientIsOperator(inviter) {
|
||||
if channel.flags.Has(InviteOnly) && !channel.ClientIsOperator(inviter) {
|
||||
inviter.ErrChanOPrivIsNeeded(channel)
|
||||
return
|
||||
}
|
||||
@@ -484,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)
|
||||
|
140
irc/client.go
140
irc/client.go
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,19 +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
|
||||
replies chan string
|
||||
username Name
|
||||
}
|
||||
|
||||
@@ -41,20 +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),
|
||||
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
|
||||
}
|
||||
@@ -63,13 +71,20 @@ func NewClient(server *Server, conn net.Conn) *Client {
|
||||
// command goroutine
|
||||
//
|
||||
|
||||
func (client *Client) run() {
|
||||
func (client *Client) writeloop() {
|
||||
for reply := range client.replies {
|
||||
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 {
|
||||
@@ -99,19 +114,55 @@ func (client *Client) run() {
|
||||
checkPass.CheckPassword()
|
||||
}
|
||||
|
||||
client.send(command)
|
||||
client.processCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) send(command Command) {
|
||||
command.SetClient(client)
|
||||
client.server.commands <- command
|
||||
func (client *Client) processCommand(cmd Command) {
|
||||
cmd.SetClient(client)
|
||||
|
||||
if !client.registered {
|
||||
regCmd, ok := cmd.(RegServerCommand)
|
||||
if !ok {
|
||||
client.Quit("unexpected command")
|
||||
return
|
||||
}
|
||||
regCmd.HandleRegServer(client.server)
|
||||
return
|
||||
}
|
||||
|
||||
srvCmd, ok := cmd.(ServerCommand)
|
||||
if !ok {
|
||||
client.ErrUnknownCommand(cmd.Code())
|
||||
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()
|
||||
|
||||
case *QuitCommand:
|
||||
// no-op
|
||||
|
||||
default:
|
||||
client.Active()
|
||||
client.Touch()
|
||||
}
|
||||
|
||||
srvCmd.HandleServer(client.server)
|
||||
}
|
||||
|
||||
// quit timer goroutine
|
||||
|
||||
func (client *Client) connectionTimeout() {
|
||||
client.send(NewQuitCommand("connection timeout"))
|
||||
client.processCommand(NewQuitCommand("connection timeout"))
|
||||
}
|
||||
|
||||
//
|
||||
@@ -143,6 +194,7 @@ func (client *Client) Touch() {
|
||||
}
|
||||
|
||||
func (client *Client) Idle() {
|
||||
client.pingTime = time.Now()
|
||||
client.Reply(RplPing(client.server))
|
||||
|
||||
if client.quitTimer == nil {
|
||||
@@ -163,12 +215,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
|
||||
|
||||
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
|
||||
@@ -180,9 +240,12 @@ func (client *Client) destroy() {
|
||||
client.quitTimer.Stop()
|
||||
}
|
||||
|
||||
close(client.replies)
|
||||
client.replies = nil
|
||||
|
||||
client.socket.Close()
|
||||
|
||||
Log.debug.Printf("%s: destroyed", client)
|
||||
log.Debugf("%s: destroyed", client)
|
||||
}
|
||||
|
||||
func (client *Client) IdleTime() time.Duration {
|
||||
@@ -210,7 +273,7 @@ func (client *Client) CanSpeak(target *Client) bool {
|
||||
isSecure := client.flags[SecureConn] && target.flags[SecureConn]
|
||||
isOperator := client.flags[Operator]
|
||||
|
||||
return requiresSecure && (isOperator || isSecure)
|
||||
return !requiresSecure || (requiresSecure && (isOperator || isSecure))
|
||||
}
|
||||
|
||||
// <mode>
|
||||
@@ -225,14 +288,25 @@ 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))
|
||||
}
|
||||
|
||||
func (c *Client) Server() Name {
|
||||
return c.server.name
|
||||
}
|
||||
|
||||
func (c *Client) ServerInfo() string {
|
||||
return c.server.description
|
||||
}
|
||||
|
||||
func (c *Client) Nick() Name {
|
||||
if c.HasNick() {
|
||||
return c.nick
|
||||
@@ -241,27 +315,29 @@ 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
|
||||
}
|
||||
|
||||
func (client *Client) SetNickname(nickname Name) {
|
||||
if client.HasNick() {
|
||||
Log.error.Printf("%s nickname already set!", client)
|
||||
log.Errorf("%s nickname already set!", client)
|
||||
return
|
||||
}
|
||||
client.nick = nickname
|
||||
@@ -275,13 +351,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) {
|
||||
@@ -296,10 +375,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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,30 +1,22 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmatch"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNickMissing = errors.New("nick missing")
|
||||
ErrNicknameInUse = errors.New("nickname in use")
|
||||
ErrNicknameMismatch = errors.New("nickname mismatch")
|
||||
wildMaskExpr = regexp.MustCompile(`\*|\?`)
|
||||
likeQuoter = strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`%`, `\%`,
|
||||
`_`, `\_`,
|
||||
`*`, `%`,
|
||||
`?`, `_`)
|
||||
)
|
||||
|
||||
func HasWildcards(mask string) bool {
|
||||
return wildMaskExpr.MatchString(mask)
|
||||
}
|
||||
|
||||
func ExpandUserHost(userhost Name) (expanded Name) {
|
||||
expanded = userhost
|
||||
// fill in missing wildcards for nicks
|
||||
@@ -37,24 +29,29 @@ func ExpandUserHost(userhost Name) (expanded Name) {
|
||||
return
|
||||
}
|
||||
|
||||
func QuoteLike(userhost Name) string {
|
||||
return likeQuoter.Replace(userhost.String())
|
||||
}
|
||||
|
||||
type ClientLookupSet struct {
|
||||
byNick map[Name]*Client
|
||||
db *ClientDB
|
||||
sync.RWMutex
|
||||
nicks map[Name]*Client
|
||||
}
|
||||
|
||||
func NewClientLookupSet() *ClientLookupSet {
|
||||
return &ClientLookupSet{
|
||||
byNick: make(map[Name]*Client),
|
||||
db: NewClientDB(),
|
||||
nicks: make(map[Name]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Count() int {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
return len(clients.nicks)
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Get(nick Name) *Client {
|
||||
return clients.byNick[nick.ToLower()]
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
return clients.nicks[nick.ToLower()]
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Add(client *Client) error {
|
||||
@@ -64,8 +61,11 @@ func (clients *ClientLookupSet) Add(client *Client) error {
|
||||
if clients.Get(client.nick) != nil {
|
||||
return ErrNicknameInUse
|
||||
}
|
||||
clients.byNick[client.Nick().ToLower()] = client
|
||||
clients.db.Add(client)
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
clients.nicks[client.Nick().ToLower()] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,96 +76,60 @@ func (clients *ClientLookupSet) Remove(client *Client) error {
|
||||
if clients.Get(client.nick) != client {
|
||||
return ErrNicknameMismatch
|
||||
}
|
||||
delete(clients.byNick, client.nick.ToLower())
|
||||
clients.db.Remove(client)
|
||||
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
delete(clients.nicks, client.nick.ToLower())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) {
|
||||
userhost = ExpandUserHost(userhost)
|
||||
set = make(ClientSet)
|
||||
rows, err := clients.db.db.Query(
|
||||
`SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`,
|
||||
QuoteLike(userhost))
|
||||
if err != nil {
|
||||
Log.error.Println("ClientLookupSet.FindAll.Query:", err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var sqlNickname string
|
||||
err := rows.Scan(&sqlNickname)
|
||||
if err != nil {
|
||||
Log.error.Println("ClientLookupSet.FindAll.Scan:", err)
|
||||
func (clients *ClientLookupSet) Range(f func(nick Name, client *Client) bool) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for nick, client := range clients.nicks {
|
||||
if !f(nick, client) {
|
||||
return
|
||||
}
|
||||
nickname := Name(sqlNickname)
|
||||
client := clients.Get(nickname)
|
||||
if client == nil {
|
||||
Log.error.Println("ClientLookupSet.FindAll: missing client:", nickname)
|
||||
continue
|
||||
}
|
||||
set.Add(client)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
set := NewClientSet()
|
||||
|
||||
userhost = ExpandUserHost(userhost)
|
||||
matcher := ircmatch.MakeMatch(userhost.String())
|
||||
|
||||
var casemappedNickMask string
|
||||
for _, client := range clients.nicks {
|
||||
casemappedNickMask = client.UserHost(false).String()
|
||||
if matcher.Match(casemappedNickMask) {
|
||||
set.Add(client)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Find(userhost Name) *Client {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
userhost = ExpandUserHost(userhost)
|
||||
row := clients.db.db.QueryRow(
|
||||
`SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\' LIMIT 1`,
|
||||
QuoteLike(userhost))
|
||||
var nickname Name
|
||||
err := row.Scan(&nickname)
|
||||
if err != nil {
|
||||
Log.error.Println("ClientLookupSet.Find:", err)
|
||||
return nil
|
||||
}
|
||||
return clients.Get(nickname)
|
||||
}
|
||||
matcher := ircmatch.MakeMatch(userhost.String())
|
||||
|
||||
//
|
||||
// client db
|
||||
//
|
||||
|
||||
type ClientDB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewClientDB() *ClientDB {
|
||||
db := &ClientDB{
|
||||
db: OpenDB(":memory:"),
|
||||
}
|
||||
stmts := []string{
|
||||
`CREATE TABLE client (
|
||||
nickname TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
||||
userhost TEXT NOT NULL COLLATE NOCASE,
|
||||
UNIQUE (nickname, userhost) ON CONFLICT REPLACE)`,
|
||||
`CREATE UNIQUE INDEX idx_nick ON client (nickname COLLATE NOCASE)`,
|
||||
`CREATE UNIQUE INDEX idx_uh ON client (userhost COLLATE NOCASE)`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
_, err := db.db.Exec(stmt)
|
||||
if err != nil {
|
||||
log.Fatal("NewClientDB: ", stmt, err)
|
||||
var casemappedNickMask string
|
||||
for _, client := range clients.nicks {
|
||||
casemappedNickMask = client.UserHost(false).String()
|
||||
if matcher.Match(casemappedNickMask) {
|
||||
return client
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *ClientDB) Add(client *Client) {
|
||||
_, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`,
|
||||
client.Nick().String(), client.UserHost().String())
|
||||
if err != nil {
|
||||
Log.error.Println("ClientDB.Add:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *ClientDB) Remove(client *Client) {
|
||||
_, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`,
|
||||
client.Nick().String())
|
||||
if err != nil {
|
||||
Log.error.Println("ClientDB.Remove:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
|
107
irc/commands.go
107
irc/commands.go
@@ -26,34 +26,38 @@ var (
|
||||
NotEnoughArgsError = errors.New("not enough arguments")
|
||||
ErrParseCommand = errors.New("failed to parse message")
|
||||
parseCommandFuncs = map[StringCode]parseCommandFunc{
|
||||
AWAY: ParseAwayCommand,
|
||||
CAP: ParseCapCommand,
|
||||
INVITE: ParseInviteCommand,
|
||||
ISON: ParseIsOnCommand,
|
||||
JOIN: ParseJoinCommand,
|
||||
KICK: ParseKickCommand,
|
||||
KILL: ParseKillCommand,
|
||||
LIST: ParseListCommand,
|
||||
MODE: ParseModeCommand,
|
||||
MOTD: ParseMOTDCommand,
|
||||
NAMES: ParseNamesCommand,
|
||||
NICK: ParseNickCommand,
|
||||
NOTICE: ParseNoticeCommand,
|
||||
ONICK: ParseOperNickCommand,
|
||||
OPER: ParseOperCommand,
|
||||
PART: ParsePartCommand,
|
||||
PASS: ParsePassCommand,
|
||||
PING: ParsePingCommand,
|
||||
PONG: ParsePongCommand,
|
||||
PRIVMSG: ParsePrivMsgCommand,
|
||||
QUIT: ParseQuitCommand,
|
||||
TIME: ParseTimeCommand,
|
||||
TOPIC: ParseTopicCommand,
|
||||
USER: ParseUserCommand,
|
||||
VERSION: ParseVersionCommand,
|
||||
WHO: ParseWhoCommand,
|
||||
WHOIS: ParseWhoisCommand,
|
||||
WHOWAS: ParseWhoWasCommand,
|
||||
AUTHENTICATE: ParseAuthenticateCommand,
|
||||
AWAY: ParseAwayCommand,
|
||||
CAP: ParseCapCommand,
|
||||
INVITE: ParseInviteCommand,
|
||||
ISON: ParseIsOnCommand,
|
||||
JOIN: ParseJoinCommand,
|
||||
KICK: ParseKickCommand,
|
||||
KILL: ParseKillCommand,
|
||||
LIST: ParseListCommand,
|
||||
MODE: ParseModeCommand,
|
||||
MOTD: ParseMOTDCommand,
|
||||
NAMES: ParseNamesCommand,
|
||||
NICK: ParseNickCommand,
|
||||
NOTICE: ParseNoticeCommand,
|
||||
ONICK: ParseOperNickCommand,
|
||||
OPER: ParseOperCommand,
|
||||
REHASH: ParseRehashCommand,
|
||||
PART: ParsePartCommand,
|
||||
PASS: ParsePassCommand,
|
||||
PING: ParsePingCommand,
|
||||
PONG: ParsePongCommand,
|
||||
PRIVMSG: ParsePrivMsgCommand,
|
||||
QUIT: ParseQuitCommand,
|
||||
TIME: ParseTimeCommand,
|
||||
LUSERS: ParseLUsersCommand,
|
||||
TOPIC: ParseTopicCommand,
|
||||
USER: ParseUserCommand,
|
||||
VERSION: ParseVersionCommand,
|
||||
WALLOPS: ParseWallopsCommand,
|
||||
WHO: ParseWhoCommand,
|
||||
WHOIS: ParseWhoisCommand,
|
||||
WHOWAS: ParseWhoWasCommand,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -180,6 +184,22 @@ func ParsePongCommand(args []string) (Command, error) {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// AUTHENTICATE <arg>
|
||||
|
||||
type AuthenticateCommand struct {
|
||||
BaseCommand
|
||||
arg string
|
||||
}
|
||||
|
||||
func ParseAuthenticateCommand(args []string) (Command, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, NotEnoughArgsError
|
||||
}
|
||||
return &AuthenticateCommand{
|
||||
arg: args[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PASS <password>
|
||||
|
||||
type PassCommand struct {
|
||||
@@ -641,6 +661,15 @@ func ParseOperCommand(args []string) (Command, error) {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
type RehashCommand struct {
|
||||
BaseCommand
|
||||
}
|
||||
|
||||
// REHASH
|
||||
func ParseRehashCommand(args []string) (Command, error) {
|
||||
return &RehashCommand{}, nil
|
||||
}
|
||||
|
||||
type CapCommand struct {
|
||||
BaseCommand
|
||||
subCommand CapSubCommand
|
||||
@@ -840,6 +869,14 @@ func ParseTimeCommand(args []string) (Command, error) {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
type LUsersCommand struct {
|
||||
BaseCommand
|
||||
}
|
||||
|
||||
func ParseLUsersCommand(args []string) (Command, error) {
|
||||
return &LUsersCommand{}, nil
|
||||
}
|
||||
|
||||
type KillCommand struct {
|
||||
BaseCommand
|
||||
nickname Name
|
||||
@@ -856,6 +893,20 @@ func ParseKillCommand(args []string) (Command, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WallopsCommand struct {
|
||||
BaseCommand
|
||||
message Text
|
||||
}
|
||||
|
||||
func ParseWallopsCommand(args []string) (Command, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, NotEnoughArgsError
|
||||
}
|
||||
return &WallopsCommand{
|
||||
message: NewText(args[0]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WhoWasCommand struct {
|
||||
BaseCommand
|
||||
nicknames []Name
|
||||
|
@@ -4,7 +4,11 @@ import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -26,17 +30,25 @@ func (conf *PassConfig) PasswordBytes() []byte {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
sync.Mutex
|
||||
filename string
|
||||
|
||||
Network struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
Server struct {
|
||||
PassConfig `yaml:",inline"`
|
||||
Database string
|
||||
Listen []string
|
||||
TLSListen map[string]*TLSConfig
|
||||
Log string
|
||||
MOTD string
|
||||
Name string
|
||||
PassConfig `yaml:",inline"`
|
||||
Listen []string
|
||||
TLSListen map[string]*TLSConfig
|
||||
Log string
|
||||
MOTD string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
Operator map[string]*PassConfig
|
||||
Account map[string]*PassConfig
|
||||
}
|
||||
|
||||
func (conf *Config) Operators() map[Name][]byte {
|
||||
@@ -47,6 +59,35 @@ func (conf *Config) Operators() map[Name][]byte {
|
||||
return operators
|
||||
}
|
||||
|
||||
func (conf *Config) Accounts() map[string][]byte {
|
||||
accounts := make(map[string][]byte)
|
||||
for name, account := range conf.Account {
|
||||
accounts[name] = []byte(account.Password)
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
func (conf *Config) Name() string {
|
||||
return conf.filename
|
||||
}
|
||||
|
||||
func (conf *Config) Reload() error {
|
||||
conf.Lock()
|
||||
defer conf.Unlock()
|
||||
|
||||
newconf, err := LoadConfig(conf.filename)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = mergo.MergeWithOverwrite(conf, newconf)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadConfig(filename string) (config *Config, err error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
@@ -58,11 +99,23 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.filename = filename
|
||||
|
||||
if config.Network.Name == "" {
|
||||
return nil, errors.New("Network name missing")
|
||||
}
|
||||
|
||||
if config.Server.Name == "" {
|
||||
return nil, errors.New("Server name missing")
|
||||
}
|
||||
|
||||
if !IsHostname(config.Server.Name) {
|
||||
return nil, errors.New("Server name must match the format of a hostname")
|
||||
}
|
||||
|
||||
if len(config.Server.Listen)+len(config.Server.TLSListen) == 0 {
|
||||
return nil, errors.New("Server listening addresses missing")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
@@ -5,36 +5,39 @@ const (
|
||||
MAX_REPLY_LEN = 512 - len(CRLF)
|
||||
|
||||
// string codes
|
||||
AWAY StringCode = "AWAY"
|
||||
CAP StringCode = "CAP"
|
||||
DEBUG StringCode = "DEBUG"
|
||||
ERROR StringCode = "ERROR"
|
||||
INVITE StringCode = "INVITE"
|
||||
ISON StringCode = "ISON"
|
||||
JOIN StringCode = "JOIN"
|
||||
KICK StringCode = "KICK"
|
||||
KILL StringCode = "KILL"
|
||||
LIST StringCode = "LIST"
|
||||
MODE StringCode = "MODE"
|
||||
MOTD StringCode = "MOTD"
|
||||
NAMES StringCode = "NAMES"
|
||||
NICK StringCode = "NICK"
|
||||
NOTICE StringCode = "NOTICE"
|
||||
ONICK StringCode = "ONICK"
|
||||
OPER StringCode = "OPER"
|
||||
PART StringCode = "PART"
|
||||
PASS StringCode = "PASS"
|
||||
PING StringCode = "PING"
|
||||
PONG StringCode = "PONG"
|
||||
PRIVMSG StringCode = "PRIVMSG"
|
||||
QUIT StringCode = "QUIT"
|
||||
TIME StringCode = "TIME"
|
||||
TOPIC StringCode = "TOPIC"
|
||||
USER StringCode = "USER"
|
||||
VERSION StringCode = "VERSION"
|
||||
WHO StringCode = "WHO"
|
||||
WHOIS StringCode = "WHOIS"
|
||||
WHOWAS StringCode = "WHOWAS"
|
||||
AUTHENTICATE StringCode = "AUTHENTICATE" // SASL
|
||||
AWAY StringCode = "AWAY"
|
||||
CAP StringCode = "CAP"
|
||||
ERROR StringCode = "ERROR"
|
||||
INVITE StringCode = "INVITE"
|
||||
ISON StringCode = "ISON"
|
||||
JOIN StringCode = "JOIN"
|
||||
KICK StringCode = "KICK"
|
||||
KILL StringCode = "KILL"
|
||||
LIST StringCode = "LIST"
|
||||
MODE StringCode = "MODE"
|
||||
MOTD StringCode = "MOTD"
|
||||
NAMES StringCode = "NAMES"
|
||||
NICK StringCode = "NICK"
|
||||
NOTICE StringCode = "NOTICE"
|
||||
ONICK StringCode = "ONICK"
|
||||
OPER StringCode = "OPER"
|
||||
REHASH StringCode = "REHASH"
|
||||
PART StringCode = "PART"
|
||||
PASS StringCode = "PASS"
|
||||
PING StringCode = "PING"
|
||||
PONG StringCode = "PONG"
|
||||
PRIVMSG StringCode = "PRIVMSG"
|
||||
QUIT StringCode = "QUIT"
|
||||
TIME StringCode = "TIME"
|
||||
LUSERS StringCode = "LUSERS"
|
||||
TOPIC StringCode = "TOPIC"
|
||||
USER StringCode = "USER"
|
||||
VERSION StringCode = "VERSION"
|
||||
WALLOPS StringCode = "WALLOPS"
|
||||
WHO StringCode = "WHO"
|
||||
WHOIS StringCode = "WHOIS"
|
||||
WHOWAS StringCode = "WHOWAS"
|
||||
|
||||
// numeric codes
|
||||
RPL_WELCOME NumericCode = 1
|
||||
@@ -90,6 +93,7 @@ const (
|
||||
RPL_LISTEND NumericCode = 323
|
||||
RPL_CHANNELMODEIS NumericCode = 324
|
||||
RPL_UNIQOPIS NumericCode = 325
|
||||
RPL_WHOISLOGGEDIN NumericCode = 330
|
||||
RPL_NOTOPIC NumericCode = 331
|
||||
RPL_TOPIC NumericCode = 332
|
||||
RPL_INVITING NumericCode = 341
|
||||
@@ -176,4 +180,15 @@ const (
|
||||
ERR_UMODEUNKNOWNFLAG NumericCode = 501
|
||||
ERR_USERSDONTMATCH NumericCode = 502
|
||||
RPL_WHOISSECURE NumericCode = 671
|
||||
|
||||
// SASL
|
||||
RPL_LOGGEDIN NumericCode = 900
|
||||
RPL_LOGGEDOUT NumericCode = 901
|
||||
ERR_NICKLOCKED NumericCode = 902
|
||||
RPL_SASLSUCCESS NumericCode = 903
|
||||
ERR_SASLFAIL NumericCode = 904
|
||||
ERR_SASLTOOLONG NumericCode = 905
|
||||
ERR_SASLABORTED NumericCode = 906
|
||||
ERR_SASLALREADY NumericCode = 907
|
||||
RPL_SASLMECHS NumericCode = 908
|
||||
)
|
||||
|
@@ -1,17 +0,0 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// OpenDB opens a connection to a sqlite3 database given a path
|
||||
func OpenDB(path string) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
log.Fatal("open db error: ", err)
|
||||
}
|
||||
return db
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Logging struct {
|
||||
debug *log.Logger
|
||||
info *log.Logger
|
||||
warn *log.Logger
|
||||
error *log.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
levels = map[string]uint8{
|
||||
"debug": 4,
|
||||
"info": 3,
|
||||
"warn": 2,
|
||||
"error": 1,
|
||||
}
|
||||
devNull io.Writer
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
devNull, err = os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogger(on bool) *log.Logger {
|
||||
return log.New(output(on), "", log.LstdFlags)
|
||||
}
|
||||
|
||||
func output(on bool) io.Writer {
|
||||
if on {
|
||||
return os.Stdout
|
||||
}
|
||||
return devNull
|
||||
}
|
||||
|
||||
func (logging *Logging) SetLevel(level string) {
|
||||
logging.debug = NewLogger(levels[level] >= levels["debug"])
|
||||
logging.info = NewLogger(levels[level] >= levels["info"])
|
||||
logging.warn = NewLogger(levels[level] >= levels["warn"])
|
||||
logging.error = NewLogger(levels[level] >= levels["error"])
|
||||
}
|
||||
|
||||
func NewLogging(level string) *Logging {
|
||||
logging := &Logging{}
|
||||
logging.SetLevel(level)
|
||||
return logging
|
||||
}
|
||||
|
||||
var (
|
||||
Log = NewLogging("warn")
|
||||
)
|
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))
|
||||
}
|
29
irc/modes.go
29
irc/modes.go
@@ -51,25 +51,25 @@ 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 (
|
||||
SupportedUserModes = UserModes{
|
||||
Invisible, Operator,
|
||||
}
|
||||
DefaultChannelModes = ChannelModes{
|
||||
NoOutside, OpOnlyTopic,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
Anonymous ChannelMode = 'a' // flag
|
||||
BanMask ChannelMode = 'b' // arg
|
||||
ChannelCreator ChannelMode = 'O' // flag
|
||||
ChannelOperator ChannelMode = 'o' // arg
|
||||
@@ -81,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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -117,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] {
|
||||
@@ -134,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
|
||||
|
25
irc/net.go
25
irc/net.go
@@ -27,3 +27,28 @@ func LookupHostname(addr Name) Name {
|
||||
hostname := strings.TrimSuffix(names[0], ".")
|
||||
return Name(hostname)
|
||||
}
|
||||
|
||||
var allowedHostnameChars = "abcdefghijklmnopqrstuvwxyz1234567890-."
|
||||
|
||||
func IsHostname(name string) bool {
|
||||
// IRC hostnames specifically require a period
|
||||
if !strings.Contains(name, ".") || len(name) < 1 || len(name) > 253 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ensure each part of hostname is valid
|
||||
for _, part := range strings.Split(name, ".") {
|
||||
if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ensure all chars of hostname are valid
|
||||
for _, char := range strings.Split(strings.ToLower(name), "") {
|
||||
if !strings.Contains(allowedHostnameChars, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
45
irc/net_test.go
Normal file
45
irc/net_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package irc
|
||||
|
||||
import "testing"
|
||||
|
||||
// hostnames from https://github.com/DanielOaks/irc-parser-tests
|
||||
var (
|
||||
goodHostnames = []string{
|
||||
"irc.example.com",
|
||||
"i.coolguy.net",
|
||||
"irc-srv.net.uk",
|
||||
"iRC.CooLguY.NeT",
|
||||
"gsf.ds342.co.uk",
|
||||
"324.net.uk",
|
||||
"xn--bcher-kva.ch",
|
||||
}
|
||||
|
||||
badHostnames = []string{
|
||||
"-lol-.net.uk",
|
||||
"-lol.net.uk",
|
||||
"_irc._sctp.lol.net.uk",
|
||||
"irc",
|
||||
"com",
|
||||
"",
|
||||
}
|
||||
)
|
||||
|
||||
func TestIsHostname(t *testing.T) {
|
||||
for _, name := range goodHostnames {
|
||||
if !IsHostname(name) {
|
||||
t.Error(
|
||||
"Expected to pass, but could not validate hostname",
|
||||
name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range badHostnames {
|
||||
if IsHostname(name) {
|
||||
t.Error(
|
||||
"Expected to fail, but successfully validated hostname",
|
||||
name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,10 +13,6 @@ func (m *NickCommand) HandleRegServer(s *Server) {
|
||||
return
|
||||
}
|
||||
|
||||
if client.capState == CapNegotiating {
|
||||
client.capState = CapNegotiated
|
||||
}
|
||||
|
||||
if m.nickname == "" {
|
||||
client.ErrNoNicknameGiven()
|
||||
return
|
||||
|
122
irc/password.go
122
irc/password.go
@@ -1,31 +1,125 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
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
|
||||
}
|
296
irc/reply.go
296
irc/reply.go
@@ -164,20 +164,27 @@ func RplKill(client *Client, target *Client, comment Text) string {
|
||||
}
|
||||
|
||||
func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string {
|
||||
return NewStringReply(nil, CAP, "%s %s :%s", client.Nick(), subCommand, arg)
|
||||
// client.server needs to be here to workaround a parsing bug in weechat 1.4
|
||||
// and let it connect to the server (otherwise it doesn't respond to the CAP
|
||||
// message with anything and just hangs on connection)
|
||||
return NewStringReply(client.server, CAP, "%s %s :%s", client.Nick(), subCommand, arg)
|
||||
}
|
||||
|
||||
// numeric replies
|
||||
|
||||
func (target *Client) RplWelcome() {
|
||||
target.NumericReply(RPL_WELCOME,
|
||||
":Welcome to the Internet Relay Network %s", target.Id())
|
||||
target.NumericReply(
|
||||
RPL_WELCOME,
|
||||
":Welcome to the %s Internet Relay Network %s",
|
||||
target.server.Network(),
|
||||
target.Id(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplYourHost() {
|
||||
target.NumericReply(
|
||||
RPL_YOURHOST,
|
||||
":Your host is %s, running version %s",
|
||||
":Your host is %s, running %s",
|
||||
target.server.name,
|
||||
FullVersion(),
|
||||
)
|
||||
@@ -231,6 +238,15 @@ func (target *Client) RplYoureOper() {
|
||||
":You are now an IRC operator")
|
||||
}
|
||||
|
||||
// <config file> :Rehashing
|
||||
func (target *Client) RplRehashing() {
|
||||
target.NumericReply(
|
||||
RPL_REHASHING,
|
||||
"%s :Rehashing",
|
||||
target.server.config.Name(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhois(client *Client) {
|
||||
target.RplWhoisUser(client)
|
||||
if client.flags[Operator] {
|
||||
@@ -242,13 +258,28 @@ func (target *Client) RplWhois(client *Client) {
|
||||
if client.flags[SecureConn] {
|
||||
target.RplWhoisSecure(client)
|
||||
}
|
||||
target.RplEndOfWhois()
|
||||
target.RplWhoisServer(client)
|
||||
target.RplWhoisLoggedIn(client)
|
||||
target.RplEndOfWhois(client)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisUser(client *Client) {
|
||||
target.NumericReply(RPL_WHOISUSER,
|
||||
"%s %s %s * :%s", client.Nick(), client.username, client.hostname,
|
||||
client.realname)
|
||||
var clientHost Name
|
||||
|
||||
if target.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) {
|
||||
@@ -270,9 +301,35 @@ func (target *Client) RplWhoisIdle(client *Client) {
|
||||
client.Nick(), client.IdleSeconds(), client.SignonTime())
|
||||
}
|
||||
|
||||
func (target *Client) RplEndOfWhois() {
|
||||
target.NumericReply(RPL_ENDOFWHOIS,
|
||||
":End of WHOIS list")
|
||||
func (target *Client) RplWhoisLoggedIn(client *Client) {
|
||||
if client.sasl.Id() == "" {
|
||||
return
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_WHOISLOGGEDIN,
|
||||
"%s %s :Is logged in as",
|
||||
client.Nick(),
|
||||
client.sasl.Id(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisServer(client *Client) {
|
||||
target.NumericReply(
|
||||
RPL_WHOISSERVER,
|
||||
"%s %s :%s",
|
||||
client.Nick(),
|
||||
client.Server(),
|
||||
client.ServerInfo(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplEndOfWhois(client *Client) {
|
||||
target.NumericReply(
|
||||
RPL_ENDOFWHOIS,
|
||||
"%s :End of WHOIS list",
|
||||
client.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplChannelModeIs(channel *Channel) {
|
||||
@@ -283,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 := ""
|
||||
|
||||
@@ -298,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
|
||||
@@ -415,8 +489,13 @@ func (target *Client) RplMOTDEnd() {
|
||||
}
|
||||
|
||||
func (target *Client) RplList(channel *Channel) {
|
||||
target.NumericReply(RPL_LIST,
|
||||
"%s %d :%s", channel, len(channel.members), channel.topic)
|
||||
target.NumericReply(
|
||||
RPL_LIST,
|
||||
"%s %d :%s",
|
||||
channel,
|
||||
channel.members.Count(),
|
||||
channel.topic,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplListEnd(server *Server) {
|
||||
@@ -430,8 +509,12 @@ func (target *Client) RplNamReply(channel *Channel) {
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisChannels(client *Client) {
|
||||
target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS,
|
||||
"%s :%s", client.Nick())
|
||||
target.MultilineReply(
|
||||
client.WhoisChannelsNames(target),
|
||||
RPL_WHOISCHANNELS,
|
||||
"%s :%s",
|
||||
client.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplVersion() {
|
||||
@@ -453,10 +536,94 @@ func (target *Client) RplTime() {
|
||||
"%s :%s", target.server.name, time.Now().Format(time.RFC1123))
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserClient() {
|
||||
target.NumericReply(
|
||||
RPL_LUSERCLIENT,
|
||||
"There are %d users and %d invisible on %d servers",
|
||||
// TODO: count global visible users
|
||||
target.server.clients.Count(),
|
||||
// TODO: count global invisible users
|
||||
0,
|
||||
// TODO: count global server connections
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserUnknown() {
|
||||
nUnknown := target.server.connections.Value() - target.server.clients.Count()
|
||||
|
||||
if nUnknown == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_LUSERUNKNOWN,
|
||||
"%d :unknown connections(s)",
|
||||
nUnknown,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserChannels() {
|
||||
nChannels := target.server.channels.Count()
|
||||
if nChannels == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_LUSERCHANNELS,
|
||||
"%d :channel(s) formed",
|
||||
nChannels,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserOp() {
|
||||
nOperators := 0
|
||||
target.server.clients.Range(func(_ Name, client *Client) bool {
|
||||
if client.flags[Operator] {
|
||||
nOperators++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if nOperators == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_LUSEROP,
|
||||
"%d :operator(s) online",
|
||||
// TODO: state store should know this
|
||||
nOperators,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserMe() {
|
||||
target.NumericReply(
|
||||
RPL_LUSERME,
|
||||
"I have %d clients and %d servers",
|
||||
target.server.clients.Count(),
|
||||
// TODO: count server connections
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
|
||||
target.NumericReply(RPL_WHOWASUSER,
|
||||
var whoWasHost Name
|
||||
|
||||
if target.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) {
|
||||
@@ -616,3 +783,84 @@ func (target *Client) ErrInviteOnlyChan(channel *Channel) {
|
||||
target.NumericReply(ERR_INVITEONLYCHAN,
|
||||
"%s :Cannot join channel (+i)", channel)
|
||||
}
|
||||
|
||||
//
|
||||
// SASL Errors / Replies
|
||||
//
|
||||
|
||||
func RplAuthenticate(client *Client, arg string) string {
|
||||
return NewStringReply(client.server, AUTHENTICATE, arg)
|
||||
}
|
||||
|
||||
func (target *Client) RplLoggedIn(authcid string) {
|
||||
target.NumericReply(
|
||||
RPL_LOGGEDIN,
|
||||
"%s %s :You are now logged in as %s",
|
||||
target, authcid, authcid,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLoggedOut() {
|
||||
target.NumericReply(
|
||||
RPL_LOGGEDIN,
|
||||
"%s :You are now logged out",
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrNickLocked() {
|
||||
target.NumericReply(
|
||||
ERR_NICKLOCKED,
|
||||
"%s :You must use a nick assigned to you",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplSaslSuccess() {
|
||||
target.NumericReply(
|
||||
RPL_SASLSUCCESS,
|
||||
"%s :SASL authentication successful",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslFail(message string) {
|
||||
target.NumericReply(
|
||||
ERR_SASLFAIL,
|
||||
"%s :SASL authentication failed: %s",
|
||||
target.Nick(), message,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslTooLong() {
|
||||
target.NumericReply(
|
||||
ERR_SASLFAIL,
|
||||
"%s :SASL message too long",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslAborted() {
|
||||
target.NumericReply(
|
||||
ERR_SASLABORTED,
|
||||
"%s :SASL authentication aborted",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslAlready() {
|
||||
target.NumericReply(
|
||||
ERR_SASLALREADY,
|
||||
"%s :You have already authenticated using SASL",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplSaslMechs(mechs ...string) {
|
||||
target.NumericReply(
|
||||
RPL_SASLMECHS,
|
||||
"%s %s :are available SASL mechanisms",
|
||||
target.Nick(),
|
||||
strings.Join(mechs, ","),
|
||||
)
|
||||
}
|
||||
|
86
irc/sasl.go
Normal file
86
irc/sasl.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
496
irc/server.go
496
irc/server.go
@@ -2,16 +2,19 @@ package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ServerCommand interface {
|
||||
@@ -25,40 +28,61 @@ type RegServerCommand interface {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
channels ChannelNameMap
|
||||
clients *ClientLookupSet
|
||||
commands chan Command
|
||||
ctime time.Time
|
||||
idle chan *Client
|
||||
motdFile string
|
||||
name Name
|
||||
newConns chan net.Conn
|
||||
operators map[Name][]byte
|
||||
password []byte
|
||||
signals chan os.Signal
|
||||
whoWas *WhoWasList
|
||||
config *Config
|
||||
metrics *Metrics
|
||||
channels *ChannelNameMap
|
||||
connections *Counter
|
||||
clients *ClientLookupSet
|
||||
ctime time.Time
|
||||
idle chan *Client
|
||||
motdFile string
|
||||
name Name
|
||||
network Name
|
||||
description string
|
||||
newConns chan net.Conn
|
||||
operators map[Name][]byte
|
||||
accounts PasswordStore
|
||||
password []byte
|
||||
signals chan os.Signal
|
||||
done chan bool
|
||||
whoWas *WhoWasList
|
||||
ids map[string]*Identity
|
||||
}
|
||||
|
||||
var (
|
||||
SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
|
||||
syscall.SIGTERM, syscall.SIGQUIT}
|
||||
SERVER_SIGNALS = []os.Signal{
|
||||
syscall.SIGINT, syscall.SIGHUP,
|
||||
syscall.SIGTERM, syscall.SIGQUIT,
|
||||
}
|
||||
)
|
||||
|
||||
func NewServer(config *Config) *Server {
|
||||
server := &Server{
|
||||
channels: make(ChannelNameMap),
|
||||
clients: NewClientLookupSet(),
|
||||
commands: make(chan Command),
|
||||
ctime: time.Now(),
|
||||
idle: make(chan *Client),
|
||||
motdFile: config.Server.MOTD,
|
||||
name: NewName(config.Server.Name),
|
||||
newConns: make(chan net.Conn),
|
||||
operators: config.Operators(),
|
||||
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
|
||||
whoWas: NewWhoWasList(100),
|
||||
config: config,
|
||||
metrics: NewMetrics("eris"),
|
||||
channels: NewChannelNameMap(),
|
||||
connections: &Counter{},
|
||||
clients: NewClientLookupSet(),
|
||||
ctime: time.Now(),
|
||||
idle: make(chan *Client),
|
||||
motdFile: config.Server.MOTD,
|
||||
name: NewName(config.Server.Name),
|
||||
network: NewName(config.Network.Name),
|
||||
description: config.Server.Description,
|
||||
newConns: make(chan net.Conn),
|
||||
operators: config.Operators(),
|
||||
accounts: NewMemoryPasswordStore(config.Accounts(), PasswordStoreOpts{}),
|
||||
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
|
||||
done: make(chan bool),
|
||||
whoWas: NewWhoWasList(100),
|
||||
ids: make(map[string]*Identity),
|
||||
}
|
||||
|
||||
log.Debugf("accounts: %v", config.Accounts())
|
||||
|
||||
// TODO: Make this configurabel?
|
||||
server.ids["global"] = NewIdentity(config.Server.Name, "global")
|
||||
|
||||
if config.Server.Password != "" {
|
||||
server.password = config.Server.PasswordBytes()
|
||||
}
|
||||
@@ -73,69 +97,126 @@ 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 loadChannelList(channel *Channel, list string, maskMode ChannelMode) {
|
||||
if list == "" {
|
||||
return
|
||||
}
|
||||
channel.lists[maskMode].AddAll(NewNames(strings.Split(list, " ")))
|
||||
func (server *Server) Wallops(message string) {
|
||||
text := NewText(message)
|
||||
server.clients.Range(func(_ Name, client *Client) bool {
|
||||
if client.flags[WallOps] {
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
client.replies <- RplNotice(server, client, text)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (server *Server) processCommand(cmd Command) {
|
||||
client := cmd.Client()
|
||||
func (server *Server) Wallopsf(format string, args ...interface{}) {
|
||||
server.Wallops(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
if !client.registered {
|
||||
regCmd, ok := cmd.(RegServerCommand)
|
||||
if !ok {
|
||||
client.Quit("unexpected command")
|
||||
return
|
||||
}
|
||||
regCmd.HandleRegServer(server)
|
||||
return
|
||||
}
|
||||
func (server *Server) Global(message string) {
|
||||
text := NewText(message)
|
||||
server.clients.Range(func(_ Name, client *Client) bool {
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
client.replies <- RplNotice(server.ids["global"], client, text)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
srvCmd, ok := cmd.(ServerCommand)
|
||||
if !ok {
|
||||
client.ErrUnknownCommand(cmd.Code())
|
||||
return
|
||||
}
|
||||
|
||||
switch srvCmd.(type) {
|
||||
case *PingCommand, *PongCommand:
|
||||
client.Touch()
|
||||
|
||||
case *QuitCommand:
|
||||
// no-op
|
||||
|
||||
default:
|
||||
client.Active()
|
||||
client.Touch()
|
||||
}
|
||||
|
||||
srvCmd.HandleServer(server)
|
||||
func (server *Server) Globalf(format string, args ...interface{}) {
|
||||
server.Global(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (server *Server) Shutdown() {
|
||||
for _, client := range server.clients.byNick {
|
||||
client.Reply(RplNotice(server, client, "shutting down"))
|
||||
}
|
||||
server.Global("shutting down...")
|
||||
}
|
||||
|
||||
func (server *Server) 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.done <- true
|
||||
}()
|
||||
|
||||
case conn := <-server.newConns:
|
||||
NewClient(server, conn)
|
||||
|
||||
case cmd := <-server.commands:
|
||||
server.processCommand(cmd)
|
||||
go NewClient(server, conn)
|
||||
|
||||
case client := <-server.idle:
|
||||
client.Idle()
|
||||
@@ -147,11 +228,18 @@ func (s *Server) acceptor(listener net.Listener) {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
Log.error.Printf("%s accept error: %s", s, err)
|
||||
log.Errorf("%s accept error: %s", s, err)
|
||||
continue
|
||||
}
|
||||
Log.debug.Printf("%s accept: %s", s, conn.RemoteAddr())
|
||||
log.Debugf("%s accept: %s", s, conn.RemoteAddr())
|
||||
|
||||
if _, ok := conn.(*tls.Conn); ok {
|
||||
s.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Inc()
|
||||
} else {
|
||||
s.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Inc()
|
||||
}
|
||||
|
||||
s.connections.Inc()
|
||||
s.newConns <- conn
|
||||
}
|
||||
}
|
||||
@@ -166,7 +254,7 @@ func (s *Server) listen(addr string) {
|
||||
log.Fatal(s, "listen error: ", err)
|
||||
}
|
||||
|
||||
Log.info.Printf("%s listening on %s", s, addr)
|
||||
log.Infof("%s listening on %s", s, addr)
|
||||
|
||||
go s.acceptor(listener)
|
||||
}
|
||||
@@ -187,7 +275,7 @@ func (s *Server) listentls(addr string, tlsconfig *TLSConfig) {
|
||||
log.Fatalf("error binding to %s: %s", addr, err)
|
||||
}
|
||||
|
||||
Log.info.Printf("%s listening on %s", s, addr)
|
||||
log.Infof("%s listening on %s (TLS)", s, addr)
|
||||
|
||||
go s.acceptor(listener)
|
||||
}
|
||||
@@ -207,6 +295,11 @@ func (s *Server) tryRegister(c *Client) {
|
||||
c.RplYourHost()
|
||||
c.RplCreated()
|
||||
c.RplMyInfo()
|
||||
|
||||
lusers := LUsersCommand{}
|
||||
lusers.SetClient(c)
|
||||
lusers.HandleServer(s)
|
||||
|
||||
s.MOTD(c)
|
||||
}
|
||||
|
||||
@@ -237,10 +330,29 @@ func (server *Server) MOTD(client *Client) {
|
||||
client.RplMOTDEnd()
|
||||
}
|
||||
|
||||
func (s *Server) Rehash() error {
|
||||
err := s.config.Reload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.motdFile = s.config.Server.MOTD
|
||||
s.name = NewName(s.config.Server.Name)
|
||||
s.network = NewName(s.config.Network.Name)
|
||||
s.description = s.config.Server.Description
|
||||
s.operators = s.config.Operators()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Id() Name {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *Server) Network() Name {
|
||||
return s.network
|
||||
}
|
||||
|
||||
func (s *Server) String() string {
|
||||
return s.name.String()
|
||||
}
|
||||
@@ -299,11 +411,99 @@ 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()
|
||||
if client.capState == CapNegotiating {
|
||||
client.capState = CapNegotiated
|
||||
}
|
||||
|
||||
server.clients.Remove(client)
|
||||
client.username, client.realname = msg.username, msg.realname
|
||||
@@ -330,7 +530,8 @@ func (m *PingCommand) HandleServer(s *Server) {
|
||||
}
|
||||
|
||||
func (m *PongCommand) HandleServer(s *Server) {
|
||||
// no-op
|
||||
v := s.metrics.Summary("client", "ping_latency_seconds")
|
||||
v.Observe(time.Now().Sub(m.Client().pingTime).Seconds())
|
||||
}
|
||||
|
||||
func (m *UserCommand) HandleServer(s *Server) {
|
||||
@@ -345,9 +546,10 @@ func (m *JoinCommand) HandleServer(s *Server) {
|
||||
client := m.Client()
|
||||
|
||||
if m.zero {
|
||||
for channel := range client.channels {
|
||||
client.channels.Range(func(channel *Channel) bool {
|
||||
channel.Part(client, client.Nick().Text())
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -359,7 +561,7 @@ func (m *JoinCommand) HandleServer(s *Server) {
|
||||
|
||||
channel := s.channels.Get(name)
|
||||
if channel == nil {
|
||||
channel = NewChannel(s, name)
|
||||
channel = NewChannel(s, name, true)
|
||||
}
|
||||
channel.Join(client, key)
|
||||
}
|
||||
@@ -416,28 +618,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
|
||||
}
|
||||
|
||||
@@ -448,22 +656,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 +682,10 @@ func (msg *WhoCommand) HandleServer(server *Server) {
|
||||
mask := msg.mask
|
||||
|
||||
if mask == "" {
|
||||
for _, channel := range server.channels {
|
||||
server.channels.Range(func(name Name, channel *Channel) bool {
|
||||
whoChannel(client, channel, friends)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if mask.IsChannel() {
|
||||
// TODO implement wildcard matching
|
||||
channel := server.channels.Get(mask)
|
||||
@@ -482,9 +693,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)
|
||||
@@ -499,11 +712,41 @@ 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) {
|
||||
client := msg.Client()
|
||||
if !client.flags[Operator] {
|
||||
client.ErrNoPrivileges()
|
||||
return
|
||||
}
|
||||
|
||||
server.Wallopsf(
|
||||
"Rehashing server config (%s)",
|
||||
client.Nick(),
|
||||
)
|
||||
|
||||
err := server.Rehash()
|
||||
if err != nil {
|
||||
server.Wallopsf(
|
||||
"ERROR: Rehashing config failed (%s)",
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
client.RplRehashing()
|
||||
}
|
||||
|
||||
func (msg *AwayCommand) HandleServer(server *Server) {
|
||||
@@ -535,6 +778,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 {
|
||||
@@ -556,6 +805,7 @@ func (msg *NoticeCommand) HandleServer(server *Server) {
|
||||
client.ErrCannotSendToUser(target.nick, "secure connection required")
|
||||
return
|
||||
}
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
target.Reply(RplNotice(client, target, msg.message))
|
||||
}
|
||||
|
||||
@@ -588,16 +838,17 @@ func (msg *ListCommand) HandleServer(server *Server) {
|
||||
}
|
||||
|
||||
if len(msg.channels) == 0 {
|
||||
for _, channel := range server.channels {
|
||||
if !client.flags[Operator] && channel.flags[Private] {
|
||||
continue
|
||||
server.channels.Range(func(name Name, channel *Channel) bool {
|
||||
if !CanSeeChannel(client, channel) {
|
||||
return true
|
||||
}
|
||||
client.RplList(channel)
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
for _, chname := range msg.channels {
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil || (!client.flags[Operator] && channel.flags[Private]) {
|
||||
if channel == nil || !CanSeeChannel(client, channel) {
|
||||
client.ErrNoSuchChannel(chname)
|
||||
continue
|
||||
}
|
||||
@@ -609,10 +860,11 @@ func (msg *ListCommand) HandleServer(server *Server) {
|
||||
|
||||
func (msg *NamesCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if len(server.channels) == 0 {
|
||||
for _, channel := range server.channels {
|
||||
if server.channels.Count() == 0 {
|
||||
server.channels.Range(func(name Name, channel *Channel) bool {
|
||||
channel.Names(client)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -664,6 +916,26 @@ func (msg *TimeCommand) HandleServer(server *Server) {
|
||||
client.RplTime()
|
||||
}
|
||||
|
||||
func (msg *LUsersCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
|
||||
client.RplLUserClient()
|
||||
client.RplLUserOp()
|
||||
client.RplLUserUnknown()
|
||||
client.RplLUserChannels()
|
||||
client.RplLUserMe()
|
||||
}
|
||||
|
||||
func (msg *WallopsCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if !client.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,9 @@ import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,10 +15,11 @@ const (
|
||||
)
|
||||
|
||||
type Socket struct {
|
||||
closed bool
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
writer *bufio.Writer
|
||||
closed bool
|
||||
closedMutex sync.RWMutex
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewSocket(conn net.Conn) *Socket {
|
||||
@@ -31,15 +35,20 @@ func (socket *Socket) String() string {
|
||||
}
|
||||
|
||||
func (socket *Socket) Close() {
|
||||
socket.closedMutex.Lock()
|
||||
defer socket.closedMutex.Unlock()
|
||||
|
||||
if socket.closed {
|
||||
return
|
||||
}
|
||||
socket.closed = true
|
||||
socket.conn.Close()
|
||||
Log.debug.Printf("%s closed", socket)
|
||||
log.Debugf("%s closed", socket)
|
||||
}
|
||||
|
||||
func (socket *Socket) Read() (line string, err error) {
|
||||
socket.closedMutex.RLock()
|
||||
defer socket.closedMutex.RUnlock()
|
||||
if socket.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
@@ -50,7 +59,7 @@ func (socket *Socket) Read() (line string, err error) {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
Log.debug.Printf("%s → %s", socket, line)
|
||||
log.Debugf("%s → %s", socket, line)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,6 +72,8 @@ func (socket *Socket) Read() (line string, err error) {
|
||||
}
|
||||
|
||||
func (socket *Socket) Write(line string) (err error) {
|
||||
socket.closedMutex.RLock()
|
||||
defer socket.closedMutex.RUnlock()
|
||||
if socket.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
@@ -80,14 +91,14 @@ func (socket *Socket) Write(line string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.debug.Printf("%s ← %s", socket, line)
|
||||
log.Debugf("%s ← %s", socket, line)
|
||||
return
|
||||
}
|
||||
|
||||
func (socket *Socket) isError(err error, dir rune) bool {
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
Log.debug.Printf("%s %c error: %s", socket, dir, err)
|
||||
log.Debugf("%s %c error: %s", socket, dir, err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ import (
|
||||
var (
|
||||
// regexps
|
||||
ChannelNameExpr = regexp.MustCompile(`^[&!#+][\pL\pN]{1,63}$`)
|
||||
NicknameExpr = regexp.MustCompile("^[\\pL\\pN\\pP\\pS]{1,32}$")
|
||||
NicknameExpr = regexp.MustCompile(`^[\pL\pN\pP\pS]{1,32}$`)
|
||||
)
|
||||
|
||||
// Names are normalized and canonicalized to remove formatting marks
|
||||
@@ -38,10 +38,24 @@ func (name Name) IsChannel() bool {
|
||||
func (name Name) IsNickname() bool {
|
||||
namestr := name.String()
|
||||
// * is used for unregistered clients
|
||||
// * is used for mask matching
|
||||
// ? is used for mask matching
|
||||
// . is used to denote server names
|
||||
// , is used as a separator by the protocol
|
||||
// ! separates username from nickname
|
||||
// @ separates nick+user from hostname
|
||||
// # is a channel prefix
|
||||
// @+ are channel membership prefixes
|
||||
if namestr == "*" || strings.Contains(namestr, ",") || strings.Contains("#@+", string(namestr[0])) {
|
||||
// ~&@%+ are channel membership prefixes
|
||||
// - is typically disallowed from first char of nicknames
|
||||
// nicknames can't start with digits
|
||||
if strings.Contains(namestr, "*") || strings.Contains(namestr, "?") ||
|
||||
strings.Contains(namestr, ".") || strings.Contains(namestr, ",") ||
|
||||
strings.Contains(namestr, "!") || strings.Contains(namestr, "@") ||
|
||||
strings.Contains("#~&@%+-1234567890", string(namestr[0])) {
|
||||
return false
|
||||
}
|
||||
// names that look like hostnames are restricted to servers, as with other ircds
|
||||
if IsHostname(namestr) {
|
||||
return false
|
||||
}
|
||||
return NicknameExpr.MatchString(namestr)
|
||||
|
324
irc/types.go
324
irc/types.go
@@ -3,110 +3,332 @@ package irc
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
//
|
||||
// simple types
|
||||
//
|
||||
|
||||
type ChannelNameMap map[Name]*Channel
|
||||
|
||||
func (channels ChannelNameMap) Get(name Name) *Channel {
|
||||
return channels[name.ToLower()]
|
||||
type Counter struct {
|
||||
sync.RWMutex
|
||||
value int
|
||||
}
|
||||
|
||||
func (channels ChannelNameMap) Add(channel *Channel) error {
|
||||
if channels[channel.name.ToLower()] != nil {
|
||||
func (c *Counter) Inc() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.value++
|
||||
}
|
||||
|
||||
func (c *Counter) Dec() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.value--
|
||||
}
|
||||
|
||||
func (c *Counter) Value() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return c.value
|
||||
}
|
||||
|
||||
// ChannelNameMap holds a mapping of channel names to *Channel structs
|
||||
// that is safe for concurrent readers and writers.
|
||||
type ChannelNameMap struct {
|
||||
sync.RWMutex
|
||||
channels map[Name]*Channel
|
||||
}
|
||||
|
||||
// NewChannelNameMap returns a new initialized *ChannelNameMap
|
||||
func NewChannelNameMap() *ChannelNameMap {
|
||||
return &ChannelNameMap{
|
||||
channels: make(map[Name]*Channel),
|
||||
}
|
||||
}
|
||||
|
||||
// Count returns the number of *Channel9s)
|
||||
func (c *ChannelNameMap) Count() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return len(c.channels)
|
||||
}
|
||||
|
||||
// Range ranges of the *Channels(s) calling f
|
||||
func (c *ChannelNameMap) Range(f func(kay Name, value *Channel) bool) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for k, v := range c.channels {
|
||||
if !f(k, v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns a *Channel given a name if it exists or a zero-value *Channel
|
||||
func (c *ChannelNameMap) Get(name Name) *Channel {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return c.channels[name.ToLower()]
|
||||
}
|
||||
|
||||
// Add adds a new *Channel if not already exists or an error otherwise
|
||||
func (c *ChannelNameMap) Add(channel *Channel) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if c.channels[channel.name.ToLower()] != nil {
|
||||
return fmt.Errorf("%s: already set", channel.name)
|
||||
}
|
||||
channels[channel.name.ToLower()] = channel
|
||||
c.channels[channel.name.ToLower()] = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
func (channels ChannelNameMap) Remove(channel *Channel) error {
|
||||
if channel != channels[channel.name.ToLower()] {
|
||||
// Remove removes a *Channel if it exists or an error otherwise
|
||||
func (c *ChannelNameMap) Remove(channel *Channel) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
if channel != c.channels[channel.name.ToLower()] {
|
||||
return fmt.Errorf("%s: mismatch", channel.name)
|
||||
}
|
||||
delete(channels, channel.name.ToLower())
|
||||
delete(c.channels, channel.name.ToLower())
|
||||
return nil
|
||||
}
|
||||
|
||||
type ChannelModeSet map[ChannelMode]bool
|
||||
// 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.1"
|
||||
Version = "1.6.3"
|
||||
|
||||
// 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,13 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
//"sync"
|
||||
|
||||
sync "github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type WhoWasList struct {
|
||||
sync.RWMutex
|
||||
buffer []*WhoWas
|
||||
start int
|
||||
end int
|
||||
@@ -10,6 +17,7 @@ type WhoWas struct {
|
||||
nickname Name
|
||||
username Name
|
||||
hostname Name
|
||||
hostmask Name
|
||||
realname Text
|
||||
}
|
||||
|
||||
@@ -20,10 +28,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 +44,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 +60,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 +73,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
|
||||
|
57
ircd.yml
57
ircd.yml
@@ -1,31 +1,42 @@
|
||||
network:
|
||||
# network name
|
||||
name: Local
|
||||
|
||||
server:
|
||||
# server name
|
||||
name: localhost
|
||||
# server name
|
||||
name: localhost.localdomain
|
||||
|
||||
# addresses to listen on
|
||||
listen:
|
||||
- ":6667"
|
||||
# server description
|
||||
description: Local Server
|
||||
|
||||
# addresses to listen on for TLS
|
||||
tlslisten:
|
||||
":6697":
|
||||
key: key.pem
|
||||
cert: cert.pem
|
||||
# addresses to listen on
|
||||
listen:
|
||||
- ":6667"
|
||||
|
||||
# password to login to the server
|
||||
# generated using "ircd genpasswd"
|
||||
#password: ""
|
||||
# addresses to listen on for TLS
|
||||
tlslisten:
|
||||
":6697":
|
||||
key: key.pem
|
||||
cert: cert.pem
|
||||
|
||||
# log level, one of error, warn, info, debug
|
||||
log: debug
|
||||
# password to login to the server
|
||||
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
|
||||
#password: ""
|
||||
|
||||
# motd filename
|
||||
motd: ircd.motd
|
||||
# motd filename
|
||||
motd: ircd.motd
|
||||
|
||||
# ircd operators
|
||||
# irc operators
|
||||
operator:
|
||||
# operator named 'admin' with password 'password'
|
||||
admin:
|
||||
# password to login with /OPER command
|
||||
# generated using "ircd genpasswd"
|
||||
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
|
||||
# operator named 'admin' with password 'password'
|
||||
admin:
|
||||
# password to login with /OPER command
|
||||
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
|
||||
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
|
||||
|
||||
# accounts (SASL)
|
||||
account:
|
||||
# username 'admin'
|
||||
admin:
|
||||
# password 'admin'
|
||||
password: JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD
|
||||
|
27
key.pem
27
key.pem
@@ -1,27 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAqwXZqhKHtR9vRgIkb628kIZmE95wr46JVsX/CLt5frpokiPV
|
||||
4ub2/dXTTryU01kOqXSeBNvR0jUig6xwPZghV2gWJ20E59Py8+O1PMuHsbheq5Uw
|
||||
vy0br2LtW60e9C03/qQsW/6ZkH9sdm5CxrWq9OpWNtCcnmn9R4Z3R031tiBo11ey
|
||||
7QLdhtTrzdeGmR3BTy10uU481FXfJFk7wvjtu6FEVgyFDzLRT1soeLdqQQXNFCmE
|
||||
higzySNHK+8sh84oXJWP9ZSSSmxBZIBO4METOnzhQb6M6i5xGvZyT1swYFiiHK+7
|
||||
p7N3whElyMCJDWlptJ62V15MK5q9dKeOvyCu2QIDAQABAoIBACteSWsKVeWFOidr
|
||||
pae4S+fuXPZO4w5xu7wIa3rgZ5fOV3QvqC5o2VtXHMWLpsXNgJ1SGDwtLhD7Lmxr
|
||||
ju7fpBzRJUUMLUFIiwRzyuLGzfswQLmMFJd2Ld4U3RHxXbcXvGpTYXhWoMR2u59w
|
||||
qHhRWy/OONyfnAfD4tl/bMHlMBjlxuO5vOcbMhMkVqD4bQ+9FXLyTDr+IZHiqm55
|
||||
AiEi8nyHbldgB4BohyJ1su+GiWLNPGPcUeV+wW2Z7RoLcALkZB+J649DV8JIf2Pv
|
||||
ZlR3bn0b6O7jYzj5xdIV1aAY4+mplKsJltT8ibw3YFmKSm2x6Q4WAi/RCYSOoTJj
|
||||
ogQ6b/ECgYEA1/TYLOj+Uott1/xEBGkHVBc3DyU0qM2bPWqwnTp2SILvGwO+U1cN
|
||||
HQql0+QO3SuqW5Eb9Iu6gvP43IQR6KwolHkIR7snx0yfWw9t6/BOT/jYaU4XWBZu
|
||||
jIppbAvm5JXKLrSf1WFogKWwvWGtlsbN0Gk+RJ4BJ7d+Ec/k8VeZZw0CgYEAyrwS
|
||||
HT1dk1dbxj00GGfZGaaw1FBU97SnEE8OSstosgPQqdcs1LfijK4e7zvkmPRp65Zr
|
||||
m6BlqpqcAr2Lf937ZLkFRzlhBh4w8E62kwCAxASutiv/CeAC42F3SzaNmtIokDPa
|
||||
kps1rmBuE/GH/KSZH2MFXwqpuejzEWMYaMFnc/0CgYAEX2nNJv6XT0lvGSWLbVD/
|
||||
q+SqgtdZH6ioCrP+ywNAHp6WznGZIGcckm2fJ7wBUHQHnJ2TSw0Av83nMSlnq6y6
|
||||
rOS1Bx4cE/oaDurr3xbG2cQQBcvFNdcRM3BFBsyrDjlkzrV5zZofGLvGaebo6aeb
|
||||
M7ZWD1j4o+eCltIM7PgnNQKBgBkEQhv3kY937Sw2KzKDxgdjSVi5nPKACUl65+GU
|
||||
3hNxYxNCKRcKpdsENW8B7gBt0JAwnJC0pzb5Ix03dHpP0xCnwB3815sgqJtOqzrS
|
||||
ihEmHsT+AteeG90hDs5qKekb28OHkoYavvIIlizB1iz3xqlX17bVowH829meZ8mt
|
||||
a+2lAoGAWZlPVXckm6s9QofCr2MT1GCZGrA7+QGeHmU0t9HnZD2wFxaPRqq6B0s5
|
||||
EMVueez/h0hhsJb3rYU/Pm1U3ehpowL0Nao413nGf0S8rmzKdLzzCr0+hSGeFZwy
|
||||
svXrFjXeupvOv/FbLJjFc7jSnqz+shwnHknnVUjgMWoiHxu40h4=
|
||||
-----END RSA PRIVATE KEY-----
|
64
main.go
64
main.go
@@ -1,58 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"syscall"
|
||||
"os"
|
||||
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/prologic/ircd/irc"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/prologic/eris/irc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
version := irc.FullVersion()
|
||||
usage := `ircd.
|
||||
Usage:
|
||||
ircd genpasswd [--conf <filename>]
|
||||
ircd run [--conf <filename>]
|
||||
ircd -h | --help
|
||||
ircd -v | --version
|
||||
Options:
|
||||
-c --conf <filename> Configuration file to use [default: ircd.yml].
|
||||
-h --help Show this screen.
|
||||
-v --version Show version.`
|
||||
var (
|
||||
version bool
|
||||
debug bool
|
||||
configfile string
|
||||
)
|
||||
|
||||
arguments, _ := docopt.Parse(usage, nil, true, version, false)
|
||||
flag.BoolVar(&version, "v", false, "display version information")
|
||||
flag.BoolVar(&debug, "d", false, "enable debug logging")
|
||||
flag.StringVar(&configfile, "c", "ircd.yml", "config file")
|
||||
flag.Parse()
|
||||
|
||||
// Special case -- We do not need to load the config file here
|
||||
if arguments["genpasswd"].(bool) {
|
||||
fmt.Print("Enter Password: ")
|
||||
bytePassword, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal("Error reading password:", err.Error())
|
||||
}
|
||||
password := string(bytePassword)
|
||||
encoded, err := irc.GenerateEncodedPassword(password)
|
||||
if err != nil {
|
||||
log.Fatalln("encoding error:", err)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println(encoded)
|
||||
return
|
||||
if version {
|
||||
fmt.Printf(irc.FullVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
configfile := arguments["--conf"].(string)
|
||||
config, err := irc.LoadConfig(configfile)
|
||||
if err != nil {
|
||||
log.Fatal("Config file did not load successfully:", err.Error())
|
||||
}
|
||||
|
||||
if arguments["run"].(bool) {
|
||||
irc.Log.SetLevel(config.Server.Log)
|
||||
server := irc.NewServer(config)
|
||||
log.Println(irc.FullVersion(), "running")
|
||||
defer log.Println(irc.FullVersion(), "exiting")
|
||||
server.Run()
|
||||
}
|
||||
irc.NewServer(config).Run()
|
||||
}
|
||||
|
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/DanielOaks/girc-go
generated
vendored
Submodule
1
vendor/github.com/DanielOaks/girc-go
generated
vendored
Submodule
Submodule vendor/github.com/DanielOaks/girc-go added at 3a2b80af9b
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/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/goshuirc/e-nfa
generated
vendored
Submodule
1
vendor/github.com/goshuirc/e-nfa
generated
vendored
Submodule
Submodule vendor/github.com/goshuirc/e-nfa added at 7071788e39
1
vendor/github.com/imdario/mergo
generated
vendored
Submodule
1
vendor/github.com/imdario/mergo
generated
vendored
Submodule
Submodule vendor/github.com/imdario/mergo added at 7fe0c75c13
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/petermattis/goid
generated
vendored
Submodule
1
vendor/github.com/petermattis/goid
generated
vendored
Submodule
Submodule vendor/github.com/petermattis/goid added at 3db12ebb2a
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 1cdba8fdde
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/sasha-s/go-deadlock
generated
vendored
Submodule
1
vendor/github.com/sasha-s/go-deadlock
generated
vendored
Submodule
Submodule vendor/github.com/sasha-s/go-deadlock added at 565eb44395
1
vendor/github.com/sirupsen/logrus
generated
vendored
Submodule
1
vendor/github.com/sirupsen/logrus
generated
vendored
Submodule
Submodule vendor/github.com/sirupsen/logrus added at 95cd2b9c79
1
vendor/golang.org/x/crypto
generated
vendored
Submodule
1
vendor/golang.org/x/crypto
generated
vendored
Submodule
Submodule vendor/golang.org/x/crypto added at b080dc9a8c
1
vendor/golang.org/x/sys
generated
vendored
Submodule
1
vendor/golang.org/x/sys
generated
vendored
Submodule
Submodule vendor/golang.org/x/sys added at 4ff8c001ce
1
vendor/golang.org/x/text
generated
vendored
Submodule
1
vendor/golang.org/x/text
generated
vendored
Submodule
Submodule vendor/golang.org/x/text added at 88f656faf3
1
vendor/gopkg.in/yaml.v2
generated
vendored
Submodule
1
vendor/gopkg.in/yaml.v2
generated
vendored
Submodule
Submodule vendor/gopkg.in/yaml.v2 added at 287cf08546
Reference in New Issue
Block a user