83 Commits

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

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

* Tweaking for channel updates

* Almost working version

* Tweaking logic for CanSpeak()

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

View File

@@ -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
View File

@@ -1,3 +1,6 @@
*~*
bin
*.db
ircd
*.pem
eris

51
.gitmodules vendored Normal file
View 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
View File

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

49
CONTRIBUTING.md Normal file
View File

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

View File

@@ -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
View File

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

View File

@@ -2,10 +2,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
View File

@@ -1,4 +1,10 @@
# ircd - IRC Daemon
# eris - IRC Server / Daemon written in Go
[![Build Status](https://travis-ci.org/prologic/eris.svg)](https://travis-ci.org/prologic/eris)
[![Go Report Card](https://goreportcard.com/badge/github.com/prologic/eris)](https://goreportcard.com/report/github.com/prologic/eris)
[![Coverage](https://coveralls.io/repos/prologic/eris/badge.svg)](https://coveralls.io/r/prologic/eris)
[![GoDoc](https://godoc.org/github.com/prologic/eris?status.svg)](https://godoc.org/github.com/prologic/eris)
[![Wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/prologic/eris/wiki)
> This project and repository is based off of [ergonomadic](https://github.com/edmund-huber/ergonomadic)
> and much of my original contributions were made in my [fork of ergonomadic](https://github.com/prologic/ergonomadic)
@@ -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.

View File

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

38
docker-compose.yml Normal file
View File

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

View File

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

View File

@@ -5,10 +5,10 @@ import (
)
type Channel struct {
flags ChannelModeSet
flags *ChannelModeSet
lists map[ChannelMode]*UserMaskSet
key Text
members MemberSet
members *MemberSet
name Name
server *Server
topic Text
@@ -17,26 +17,32 @@ type Channel struct {
// NewChannel creates a new channel from a `Server` and a `name`
// string, which must be unique on the server.
func NewChannel(s *Server, name Name) *Channel {
func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
channel := &Channel{
flags: make(ChannelModeSet),
flags: NewChannelModeSet(),
lists: map[ChannelMode]*UserMaskSet{
BanMask: NewUserMaskSet(),
ExceptMask: NewUserMaskSet(),
InviteMask: NewUserMaskSet(),
},
members: make(MemberSet),
members: NewMemberSet(),
name: name,
server: s,
}
if addDefaultModes {
for _, mode := range DefaultChannelModes {
channel.flags.Set(mode)
}
}
s.channels.Add(channel)
return channel
}
func (channel *Channel) IsEmpty() bool {
return len(channel.members) == 0
return channel.members.Count() == 0
}
func (channel *Channel) Names(client *Client) {
@@ -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)

View File

@@ -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
})
}
}

View File

@@ -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
}
//

View File

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

View File

@@ -4,7 +4,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
}

View File

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

View File

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

View File

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

194
irc/metrics.go Normal file
View File

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

View File

@@ -51,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

View File

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

45
irc/net_test.go Normal file
View File

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

View File

@@ -13,10 +13,6 @@ func (m *NickCommand) HandleRegServer(s *Server) {
return
}
if client.capState == CapNegotiating {
client.capState = CapNegotiated
}
if m.nickname == "" {
client.ErrNoNicknameGiven()
return

View File

@@ -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
View File

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

View File

@@ -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
View 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
}

View File

@@ -2,16 +2,19 @@ package irc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"log"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
log "github.com/sirupsen/logrus"
)
type ServerCommand interface {
@@ -25,40 +28,61 @@ type RegServerCommand interface {
}
type Server struct {
channels ChannelNameMap
clients *ClientLookupSet
commands chan Command
ctime time.Time
idle chan *Client
motdFile string
name Name
newConns chan net.Conn
operators map[Name][]byte
password []byte
signals chan os.Signal
whoWas *WhoWasList
config *Config
metrics *Metrics
channels *ChannelNameMap
connections *Counter
clients *ClientLookupSet
ctime time.Time
idle chan *Client
motdFile string
name Name
network Name
description string
newConns chan net.Conn
operators map[Name][]byte
accounts PasswordStore
password []byte
signals chan os.Signal
done chan bool
whoWas *WhoWasList
ids map[string]*Identity
}
var (
SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
syscall.SIGTERM, syscall.SIGQUIT}
SERVER_SIGNALS = []os.Signal{
syscall.SIGINT, syscall.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] {

View File

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

View File

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

View File

@@ -3,110 +3,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
View File

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

View File

@@ -1,11 +1,18 @@
package irc
import (
"fmt"
)
var (
//PackageName package name
Package = "eris"
// Version release version
Version = "1.5.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)
}

View File

@@ -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

View File

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

27
key.pem
View File

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

64
main.go
View File

@@ -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
View File

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

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

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

1
vendor/github.com/goshuirc/e-nfa generated vendored Submodule

1
vendor/github.com/imdario/mergo generated vendored Submodule

1
vendor/github.com/petermattis/goid generated vendored Submodule

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

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

1
vendor/github.com/sirupsen/logrus generated vendored Submodule

1
vendor/golang.org/x/crypto 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/gopkg.in/yaml.v2 generated vendored Submodule

Submodule vendor/gopkg.in/yaml.v2 added at 287cf08546