Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
eea24e3047 | ||
![]() |
e91312a468 | ||
![]() |
065a93d56f | ||
![]() |
fbbf36d1a1 | ||
![]() |
3675251706 | ||
![]() |
6f61b673a1 | ||
![]() |
4566b2021f | ||
![]() |
e7c5b96a6a | ||
![]() |
b18403ea71 | ||
![]() |
c9cbab6769 | ||
![]() |
176aba3c99 | ||
![]() |
d814c48dce | ||
![]() |
3af82e3e8e | ||
![]() |
8a8d7b1e97 | ||
![]() |
2ac33b7d2c | ||
![]() |
a54031de9e | ||
![]() |
a49dea57d8 | ||
![]() |
114c6aa80c | ||
![]() |
414c2fcf89 | ||
![]() |
9d860692fa | ||
![]() |
98cb66559a | ||
![]() |
e14795818f | ||
![]() |
c394ea6735 | ||
![]() |
c94884fb9f | ||
![]() |
784039998f | ||
![]() |
6981b10763 | ||
![]() |
d170e01d38 | ||
![]() |
5787059d11 | ||
![]() |
2e4ff30276 | ||
![]() |
f18765a41a | ||
![]() |
d23f7cf93d | ||
![]() |
6d64a46466 | ||
![]() |
14ed3a6633 | ||
![]() |
cb46494733 | ||
![]() |
e3fea6c97b | ||
![]() |
9b70d25143 | ||
![]() |
7a20037194 | ||
![]() |
5fa7214853 | ||
![]() |
e905b44fb4 | ||
![]() |
84a36a0095 | ||
![]() |
aa4907d8ae | ||
![]() |
facfcba232 | ||
![]() |
d3285748f9 | ||
![]() |
283ef104a4 | ||
![]() |
04d907d1e9 | ||
![]() |
d74a6780fe | ||
![]() |
d7e9ef230a | ||
![]() |
75f224a7c0 | ||
![]() |
caab002d51 | ||
![]() |
7ff892bba9 | ||
![]() |
233238b709 | ||
![]() |
59e0792db1 | ||
![]() |
962b6645c1 | ||
![]() |
cee8bf9957 | ||
![]() |
9d93bca179 | ||
![]() |
ccae795335 | ||
![]() |
862eb429d4 | ||
![]() |
9e075dde67 | ||
![]() |
20be29bcef | ||
![]() |
34c3be0a88 | ||
![]() |
be246a3bc4 | ||
![]() |
4fb452b2c0 | ||
![]() |
d707382a78 | ||
![]() |
7620a3c282 | ||
![]() |
18a3e2f2c3 | ||
![]() |
d046a9863f | ||
![]() |
a1450a81d6 | ||
![]() |
d594386658 | ||
![]() |
89b512fc76 | ||
![]() |
d01bb4fe57 | ||
![]() |
2fef0feb5a | ||
![]() |
735458ffed | ||
![]() |
02427bcb3f | ||
![]() |
bdcb4c21a5 | ||
![]() |
0e3be3f34c | ||
![]() |
19e564ed2b | ||
![]() |
ef10282a37 | ||
![]() |
3a9d1fefc8 | ||
![]() |
f5d8f22220 | ||
![]() |
062e2546ab | ||
![]() |
8f269b5201 | ||
![]() |
d33d60353c | ||
![]() |
9a5862287b | ||
![]() |
46d22a71b3 | ||
![]() |
4d97e035d2 | ||
![]() |
41b6511cec | ||
![]() |
1cde7c6902 | ||
![]() |
edfd990d59 | ||
![]() |
768f4f215a | ||
![]() |
9601098872 | ||
![]() |
d97fc927ad | ||
![]() |
28ed5cc2c0 | ||
![]() |
4ff06efab8 | ||
![]() |
51e1a93a99 | ||
![]() |
db4a9a864e | ||
![]() |
e333eb6029 | ||
![]() |
700c242e35 | ||
![]() |
c2512ca082 | ||
![]() |
7e41395abd | ||
![]() |
c1110f8b81 | ||
![]() |
87663a4175 | ||
![]() |
988820efb3 | ||
![]() |
91212c3254 | ||
![]() |
02b3525ef7 | ||
![]() |
12d562c0fa | ||
![]() |
ec084f49ab |
8
.dependabot/config.yml
Normal file
8
.dependabot/config.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 1
|
||||
update_configs:
|
||||
- package_manager: "go:modules"
|
||||
directory: "/"
|
||||
update_schedule: "daily"
|
||||
- package_manager: "docker"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
30
.drone.yml
30
.drone.yml
@@ -1,30 +0,0 @@
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/github.com/prologic/ircd
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
image: golang
|
||||
commands:
|
||||
- go get -d
|
||||
- go build .
|
||||
|
||||
docker:
|
||||
image: plugins/docker
|
||||
repo: r.mills.io/prologic/ircd
|
||||
registry: r.mills.io
|
||||
secrets: [ docker_username, docker_password ]
|
||||
|
||||
notify:
|
||||
image: drillster/drone-email
|
||||
host: mail.mills.io
|
||||
from: drone@mills.io
|
||||
skip_verify: true
|
||||
when:
|
||||
status: [ changed, failure ]
|
||||
|
||||
secrets:
|
||||
registry_username:
|
||||
external: true
|
||||
registry_password:
|
||||
external: true
|
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
35
.github/workflows/build.yml
vendored
Normal file
35
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.12.x"
|
||||
- "1.13.x"
|
||||
- "1.14.x"
|
||||
os:
|
||||
- "ubuntu-latest"
|
||||
- "macos-latest"
|
||||
- "windows-latest"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Setup Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: |
|
||||
go build -v .
|
||||
- name: Test
|
||||
run: |
|
||||
go test -v -race .
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
*~*
|
||||
*~
|
||||
*.db
|
||||
*.bak
|
||||
*.pem
|
||||
|
||||
eris
|
||||
/bin
|
||||
/dist
|
||||
/eris
|
||||
/coverage.txt
|
||||
|
27
.gitmodules
vendored
27
.gitmodules
vendored
@@ -1,27 +0,0 @@
|
||||
[submodule "vendor/github.com/docopt/docopt-go"]
|
||||
path = vendor/github.com/docopt/docopt-go
|
||||
url = https://github.com/docopt/docopt-go
|
||||
[submodule "vendor/github.com/sirupsen/logrus"]
|
||||
path = vendor/github.com/sirupsen/logrus
|
||||
url = https://github.com/sirupsen/logrus
|
||||
[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
|
31
.goreleaser.yml
Normal file
31
.goreleaser.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
builds:
|
||||
- binary: eris
|
||||
flags: -tags "static_build"
|
||||
ldflags: -w -X mail.Version={{.Version}} -X main.Commit={{.Commit}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- darwin
|
||||
- freebsd
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- i386
|
||||
- amd64
|
||||
- arm
|
||||
- amd64
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
sign:
|
||||
artifacts: checksum
|
||||
archive:
|
||||
wrap_in_directory: true
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- "*.pem"
|
||||
- "*.yml"
|
||||
- "LICENSE"
|
||||
- "README.md"
|
29
.yamllint.yml
Normal file
29
.yamllint.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
yaml-files:
|
||||
- '*.yaml'
|
||||
- '*.yml'
|
||||
- '.yamllint'
|
||||
|
||||
rules:
|
||||
braces: enable
|
||||
brackets: enable
|
||||
colons: enable
|
||||
commas: enable
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-end: disable
|
||||
document-start:
|
||||
level: warning
|
||||
empty-lines: enable
|
||||
empty-values: disable
|
||||
hyphens: enable
|
||||
indentation: enable
|
||||
key-duplicates: enable
|
||||
key-ordering: disable
|
||||
line-length: disable
|
||||
new-line-at-end-of-file: enable
|
||||
new-lines: enable
|
||||
octal-values: enable
|
||||
quoted-strings: disable
|
||||
trailing-spaces: enable
|
||||
truthy: disable
|
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at prologic@shortcircuit.net.au. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Welcome to the contributing guide for eris!
|
||||
|
||||
## Developers
|
||||
|
||||
- James Mills [@prologic](https://github.com/prologic) (*uthor / Maintainer*)
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@bear](https://github.com/bear)
|
||||
- [@DanielOaks](https://github.com/DanielOaks)
|
||||
- [@kzisme](https://github.com/kzisme)
|
||||
|
||||
## New Features
|
||||
|
||||
* [Create an Issue](https://github.com/prologic/eris/issues/new)
|
||||
|
||||
* [Fork eris](https://github.com/prologic/eris#fork-destination-box)
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:myuser/eris.git
|
||||
```
|
||||
|
||||
* Create a new feature branch:
|
||||
|
||||
```bash
|
||||
$ git checkout -b myfeature#issueN master
|
||||
```
|
||||
|
||||
* Hack on your feature with your favorite editor
|
||||
* Commit and Push your changes up:
|
||||
|
||||
```bash
|
||||
$ git add -A
|
||||
$ git commit -m "my fancy new feature"
|
||||
$ git push -u origin my-feature
|
||||
```
|
||||
|
||||
* Create a new [Pull Request](https://github.com/prologic/eris/compare/)
|
||||
* Give the pull request an appropriate title possibly matching the issue
|
||||
* In the pull request's description include the text `Closes #N` or `Fixes #N`
|
||||
|
||||
# Reporting Bugs
|
||||
|
||||
* File a new [Bug Report](https://github.com/prologic/eris/issues/new)
|
||||
* Label it as a "Bug"
|
||||
|
||||
When describing your bug report; please be concise and as detailed as you can
|
||||
so we can easily work out what the problem is. It's also very helpful if you
|
||||
are able to provide a test case that repeatedly demonstrates the bug at hand.
|
@@ -2,7 +2,6 @@
|
||||
FROM golang:alpine AS build
|
||||
|
||||
ARG TAG
|
||||
ARG BUILD
|
||||
|
||||
ENV APP eris
|
||||
ENV REPO prologic/$APP
|
||||
@@ -12,7 +11,7 @@ RUN apk add --update git make build-base && \
|
||||
|
||||
WORKDIR /go/src/github.com/$REPO
|
||||
COPY . /go/src/github.com/$REPO
|
||||
RUN make TAG=$TAG BUILD=$BUILD build
|
||||
RUN make TAG=$TAG build
|
||||
|
||||
# Runtime
|
||||
FROM alpine
|
||||
@@ -27,4 +26,4 @@ COPY --from=build /go/src/github.com/${REPO}/${APP} /${APP}
|
||||
EXPOSE 6667/tcp 6697/tcp
|
||||
|
||||
ENTRYPOINT ["/eris"]
|
||||
CMD ["run"]
|
||||
CMD [""]
|
||||
|
18
Makefile
18
Makefile
@@ -6,7 +6,6 @@ APP=eris
|
||||
PACKAGE=irc
|
||||
REPO?=prologic/$(APP)
|
||||
TAG?=latest
|
||||
BUILD?=-dev
|
||||
|
||||
all: dev
|
||||
|
||||
@@ -17,17 +16,24 @@ deps:
|
||||
@go get ./...
|
||||
|
||||
build: clean deps
|
||||
@echo " -> Building $(TAG)$(BUILD)"
|
||||
@echo "github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT)"
|
||||
@echo " -> Building $(REPO) $(TAG)@$(COMMIT)"
|
||||
@go build -tags "netgo static_build" -installsuffix netgo \
|
||||
-ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT) -X github.com/$(REPO)/${PACKAGE}.Build=$(BUILD)" .
|
||||
-ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT)"
|
||||
@echo "Built $$(./$(APP) -v)"
|
||||
|
||||
image:
|
||||
@docker build --build-arg TAG=$(TAG) --build-arg BUILD=$(BUILD) -t $(REPO):$(TAG) .
|
||||
@docker build --build-arg TAG=$(TAG) -t $(REPO):$(TAG) .
|
||||
@echo "Image created: $(REPO):$(TAG)"
|
||||
|
||||
profile:
|
||||
@go test -cpuprofile cpu.prof -memprofile mem.prof -v -bench ./...
|
||||
|
||||
bench:
|
||||
@go test -v -bench ./...
|
||||
|
||||
test:
|
||||
@go test -v -cover -race $(TEST_ARGS)
|
||||
@go test -v -cover -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -race ./...
|
||||
|
||||
clean:
|
||||
@rm -rf $(APP)
|
||||
@git clean -f -d -X
|
||||
|
3
PULL_REQUEST_TEMPLATE.md
Normal file
3
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
<one line description here>
|
||||
|
||||
Fixes #xx
|
124
README.md
124
README.md
@@ -1,4 +1,10 @@
|
||||
# eris - IRC Server / Daemon written in Go
|
||||
eris - IRC Server / Daemon written in Go
|
||||
|
||||
[](https://cloud.drone.io/prologic/eris)
|
||||
[](https://codecov.io/gh/prologic/eris)
|
||||
[](https://goreportcard.com/report/prologic/eris)
|
||||
[](https://godoc.org/github.com/prologic/eris)
|
||||
[](https://sourcegraph.com/github.com/prologic/eris?badge)
|
||||
|
||||
> This project and repository is based off of [ergonomadic](https://github.com/edmund-huber/ergonomadic)
|
||||
> and much of my original contributions were made in my [fork of ergonomadic](https://github.com/prologic/ergonomadic)
|
||||
@@ -19,14 +25,19 @@ 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
|
||||
So `eris` is an IRC daemon written from scratch in Go to facilitate discord
|
||||
and have arguments for the sake of argument!
|
||||
|
||||
Pull requests and issues are welcome.
|
||||
|
||||
Discussion at:
|
||||
|
||||
* /server irc.mills.io:6697 (*use SSL*)
|
||||
* /server irc.mills.io +6697 (*use TLS/SSL*)
|
||||
* /join #lobby
|
||||
|
||||
Or (**not recommended**):
|
||||
|
||||
* /server irc.mills.io (*default port 6667, non-TLS*)
|
||||
* /join #lobby
|
||||
|
||||
## Features
|
||||
@@ -42,6 +53,42 @@ Discussion at:
|
||||
* SSL/TLS support
|
||||
* Simple IRC operator privileges (*overrides most things*)
|
||||
* Secure connection tracking (+z) and SecureOnly user mode (+Z)
|
||||
* Secure channels (+Z)
|
||||
* Three layers of channel privacy, Public, Private (+p) and Secret (s)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```#!bash
|
||||
$ go get github.com/prologic/eris
|
||||
$ cat > ircd.yml <<EOF
|
||||
network:
|
||||
name: Test
|
||||
server:
|
||||
name: Test
|
||||
listen:
|
||||
- ":6667"
|
||||
EOF
|
||||
$ eris
|
||||
```
|
||||
|
||||
If you want TLS (**recommended**) then:
|
||||
|
||||
```#!bash
|
||||
$ go get github.com/prologic/mksslcert
|
||||
$ mksslcert
|
||||
```
|
||||
|
||||
This generates a self-signed cert `cert.pem` and `key.pem` into the `$PWD`.
|
||||
|
||||
Then add a `tlslisten` block to your config:
|
||||
|
||||
```#!yaml
|
||||
server:
|
||||
tlslisten:
|
||||
":6697":
|
||||
key: key.pem
|
||||
cert: cert.pem
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -53,18 +100,81 @@ $ eris --help
|
||||
## Configuration
|
||||
|
||||
See the example [ircd.yml](ircd.yml). Passwords are base64-encoded
|
||||
bcrypted byte strings. You can generate them with the `genpasswd` subcommand.
|
||||
bcrypted byte strings. You can generate them with the `mkpasswd` tool
|
||||
from [prologic/mkpasswd](https://github.com/prologic/mkpasswd):
|
||||
|
||||
```#!bash
|
||||
$ eris genpasswd
|
||||
$ go install github.com/prologic/mkpasswd
|
||||
$ mkpasswd
|
||||
```
|
||||
|
||||
## Running the server
|
||||
Self-signed certificates can also be generated using the `mksslcert` tool
|
||||
from [prologic/mksslcert](https://github.com/prologic/mksslcert):
|
||||
|
||||
```#!bash
|
||||
$ eris run
|
||||
$ go install github.com/prologic/mksslcert
|
||||
$ mksslcert
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*):
|
||||
|
||||
```#!bash
|
||||
$ eris
|
||||
```
|
||||
|
||||
Or you can deploy with [Docker](https://www.docker.com) using the prebuilt [prologic/eris](https://hub.docker.com/r/prologic/eris/):
|
||||
|
||||
```#!bash
|
||||
docker run -d -p 6667:6667 -p 6697:6697 prologic/eris
|
||||
```
|
||||
|
||||
You may want to customize the configuration however and create your own image based off of this; or deploy with `docker stack deploy` on a [Docker Swarm](https://docs.docker.com/engine/swarm/) clsuter like this:
|
||||
|
||||
```#!bash
|
||||
$ docker stack deploy -c docker-compose.yml eris
|
||||
```
|
||||
|
||||
Which assumes a `ircd.yml` coniguration file in the current directory which Docker will use to distribute as the configuration. The `docker-compose.yml` (*Docker Stackfile*) is available at the root of this repository.
|
||||
|
||||
## Related Projects
|
||||
|
||||
There are a number of supported accompanying services that are being developed alongside Eris:
|
||||
|
||||
* [Soter](https://github.com/prologic/soter) -- An IRC Bot that persists channel modes and topics.
|
||||
* [Cadmus](https://github.com/prologic/cadmus) -- An IRC Bot that logs channels and provides an interface for viewing and searching logs
|
||||
|
||||
## Recommended Clients
|
||||
|
||||
### CLI / Terminal
|
||||
|
||||
* [irccat](https://github.com/prologic/irccat)
|
||||
* [irssi](https://irssi.org/)
|
||||
|
||||
### Cloud
|
||||
|
||||
* [IRCCloud](https://www.irccloud.com/)
|
||||
|
||||
### Desktop
|
||||
|
||||
* [HexChat (Linux)](https://hexchat.github.io/)
|
||||
* [Textual (OSX)](https://www.codeux.com/textual/)
|
||||
* [mIRC (Windows)](https://www.mirc.com/)
|
||||
|
||||
### Mobile
|
||||
|
||||
* [Palaver (iOS)](https://palaverapp.com/) -- SASL, TLS, Server Password, Push Notifications, IRCv3 (*Also supports custom image upload service(s) for better privacy of shared photos/images over IRC*)
|
||||
|
||||
### Web
|
||||
|
||||
* [Dispatch](https://github.com/khlieng/dispatch) -- TLS, Multiple Servers and Users, Client Certificates
|
||||
|
||||
## Related Projects
|
||||
|
||||
* [cadmus](https://github.com/prologic/cadmus) -- an IRC Bot written in Go that logs IRC Channels and provides an interface to view and search those logs
|
||||
* [soter](https://github.com/prologic/soter) -- an IRC Bot written in Go that protects IRC Channels by persisting channel modes and topics
|
||||
|
||||
## License
|
||||
|
||||
eris is licensed under the MIT License.
|
||||
|
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-architect
|
38
docker-compose.yml
Normal file
38
docker-compose.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
version: "3.3"
|
||||
|
||||
services:
|
||||
eris:
|
||||
image: prologic/eris
|
||||
configs:
|
||||
- source: ircd_yml
|
||||
target: /ircd.yml
|
||||
- source: ircd_motd
|
||||
target: /ircd.motd
|
||||
- source: cert_pem
|
||||
target: /cert.pem
|
||||
- source: key_pem
|
||||
target: /key.pem
|
||||
ports:
|
||||
- target: 6667
|
||||
published: 6667
|
||||
protocol: tcp
|
||||
mode: host
|
||||
- target: 6697
|
||||
published: 6697
|
||||
protocol: tcp
|
||||
mode: host
|
||||
deploy:
|
||||
endpoint_mode: dnsrr
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
replicas: 1
|
||||
|
||||
configs:
|
||||
ircd_yml:
|
||||
file: ./ircd.yml
|
||||
ircd_motd:
|
||||
file: ./ircd.motd
|
||||
cert_pem:
|
||||
file: ./cert.pem
|
||||
key_pem:
|
||||
file: ./key.pem
|
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module github.com/prologic/eris
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287
|
||||
github.com/google/uuid v1.1.0 // indirect
|
||||
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 // indirect
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3
|
||||
github.com/prometheus/client_golang v0.9.4
|
||||
github.com/renstrom/shortuuid v3.0.0+incompatible
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
golang.org/x/text v0.3.4
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
100
go.sum
Normal file
100
go.sum
Normal file
@@ -0,0 +1,100 @@
|
||||
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287 h1:xOE8jDDulcwdPG+coLps6seNn6yERt5xgKSATNqWUM0=
|
||||
github.com/DanielOaks/girc-go v0.0.0-20180430075055-8d136c4f9287/go.mod h1:nn+Gr++RLey8iGwfvI84UO5oZal6Muz7qPxDII0BsQ8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s=
|
||||
github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940 h1:KmRLPRstEJiE/9OjumKqI8Rccip8Qmyw2FwyTFxtVqs=
|
||||
github.com/goshuirc/e-nfa v0.0.0-20160917075329-7071788e3940/go.mod h1:VOmrX6cmj7zwUeexC9HzznUdTIObHqIXUrWNYS+Ik7w=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3 h1:2YMbJ6WbdQI9K73chxh9OWMDsZ2PNjAIRGTonp3T0l0=
|
||||
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.4 h1:Y8E/JaaPbmFSW2V81Ab/d8yZFYQQGbni1b1jPcG9Y6A=
|
||||
github.com/prometheus/client_golang v0.9.4/go.mod h1:oCXIBxdI62A4cR6aTRJCgetEjecSIYzOEaeAn4iYEpM=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/renstrom/shortuuid v3.0.0+incompatible h1:F6T1U7bWlI3FTV+JE8HyeR7bkTeYZJntqQLA9ST4HOQ=
|
||||
github.com/renstrom/shortuuid v3.0.0+incompatible/go.mod h1:n18Ycpn8DijG+h/lLBQVnGKv1BCtTeXo8KKSbBOrQ8c=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3 h1:389FrrKIAlxqQMTscCQ7VH3JAVuxb/pe53v2LBiA7z8=
|
||||
github.com/thoj/go-ircevent v0.0.0-20180816043103-14f3614f28c3/go.mod h1:QYOctLs5qEsaIrA/PKEc4YqAv2SozbxNEX0vMPs84p4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
1403
grafana/Eris-1525253970771.json
Normal file
1403
grafana/Eris-1525253970771.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ const (
|
||||
var (
|
||||
SupportedCapabilities = CapabilitySet{
|
||||
MultiPrefix: true,
|
||||
SASL: true,
|
||||
}
|
||||
)
|
||||
|
||||
|
136
irc/channel.go
136
irc/channel.go
@@ -5,10 +5,10 @@ import (
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
flags ChannelModeSet
|
||||
flags *ChannelModeSet
|
||||
lists map[ChannelMode]*UserMaskSet
|
||||
key Text
|
||||
members MemberSet
|
||||
members *MemberSet
|
||||
name Name
|
||||
server *Server
|
||||
topic Text
|
||||
@@ -19,20 +19,20 @@ type Channel struct {
|
||||
// string, which must be unique on the server.
|
||||
func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
|
||||
channel := &Channel{
|
||||
flags: make(ChannelModeSet),
|
||||
flags: NewChannelModeSet(),
|
||||
lists: map[ChannelMode]*UserMaskSet{
|
||||
BanMask: NewUserMaskSet(),
|
||||
ExceptMask: NewUserMaskSet(),
|
||||
InviteMask: NewUserMaskSet(),
|
||||
},
|
||||
members: make(MemberSet),
|
||||
members: NewMemberSet(),
|
||||
name: name,
|
||||
server: s,
|
||||
}
|
||||
|
||||
if addDefaultModes {
|
||||
for _, mode := range DefaultChannelModes {
|
||||
channel.flags[mode] = true
|
||||
channel.flags.Set(mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewChannel(s *Server, name Name, addDefaultModes bool) *Channel {
|
||||
}
|
||||
|
||||
func (channel *Channel) IsEmpty() bool {
|
||||
return len(channel.members) == 0
|
||||
return channel.members.Count() == 0
|
||||
}
|
||||
|
||||
func (channel *Channel) Names(client *Client) {
|
||||
@@ -51,31 +51,34 @@ func (channel *Channel) Names(client *Client) {
|
||||
}
|
||||
|
||||
func (channel *Channel) ClientIsOperator(client *Client) bool {
|
||||
return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator)
|
||||
return client.modes.Has(Operator) || channel.members.HasMode(client, ChannelOperator)
|
||||
}
|
||||
|
||||
func (channel *Channel) Nicks(target *Client) []string {
|
||||
isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix]
|
||||
nicks := make([]string, len(channel.members))
|
||||
channel.members.RLock()
|
||||
defer channel.members.RUnlock()
|
||||
nicks := make([]string, channel.members.Count())
|
||||
i := 0
|
||||
for client, modes := range channel.members {
|
||||
channel.members.Range(func(client *Client, modes *ChannelModeSet) bool {
|
||||
if isMultiPrefix {
|
||||
if modes[ChannelOperator] {
|
||||
if modes.Has(ChannelOperator) {
|
||||
nicks[i] += "@"
|
||||
}
|
||||
if modes[Voice] {
|
||||
if modes.Has(Voice) {
|
||||
nicks[i] += "+"
|
||||
}
|
||||
} else {
|
||||
if modes[ChannelOperator] {
|
||||
if modes.Has(ChannelOperator) {
|
||||
nicks[i] += "@"
|
||||
} else if modes[Voice] {
|
||||
} else if modes.Has(Voice) {
|
||||
nicks[i] += "+"
|
||||
}
|
||||
}
|
||||
nicks[i] += client.Nick().String()
|
||||
i += 1
|
||||
}
|
||||
i++
|
||||
return true
|
||||
})
|
||||
return nicks
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ func (channel *Channel) String() string {
|
||||
|
||||
// <mode> <mode params>
|
||||
func (channel *Channel) ModeString(client *Client) (str string) {
|
||||
isMember := client.flags[Operator] || channel.members.Has(client)
|
||||
isMember := client.modes.Has(Operator) || channel.members.Has(client)
|
||||
showKey := isMember && (channel.key != "")
|
||||
showUserLimit := channel.userLimit > 0
|
||||
|
||||
@@ -106,9 +109,10 @@ func (channel *Channel) ModeString(client *Client) (str string) {
|
||||
}
|
||||
|
||||
// flags
|
||||
for mode := range channel.flags {
|
||||
channel.flags.Range(func(mode ChannelMode) bool {
|
||||
str += mode.String()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
str = "+" + str
|
||||
|
||||
@@ -126,7 +130,7 @@ func (channel *Channel) ModeString(client *Client) (str string) {
|
||||
|
||||
func (channel *Channel) IsFull() bool {
|
||||
return (channel.userLimit > 0) &&
|
||||
(uint64(len(channel.members)) >= channel.userLimit)
|
||||
(uint64(channel.members.Count()) >= channel.userLimit)
|
||||
}
|
||||
|
||||
func (channel *Channel) CheckKey(key Text) bool {
|
||||
@@ -151,31 +155,32 @@ func (channel *Channel) Join(client *Client, key Text) {
|
||||
return
|
||||
}
|
||||
|
||||
isInvited := channel.lists[InviteMask].Match(client.UserHost())
|
||||
if !isOperator && channel.flags[InviteOnly] && !isInvited {
|
||||
isInvited := channel.lists[InviteMask].Match(client.UserHost(false))
|
||||
if !isOperator && channel.flags.Has(InviteOnly) && !isInvited {
|
||||
client.ErrInviteOnlyChan(channel)
|
||||
return
|
||||
}
|
||||
|
||||
if channel.lists[BanMask].Match(client.UserHost()) &&
|
||||
if channel.lists[BanMask].Match(client.UserHost(false)) &&
|
||||
!isInvited &&
|
||||
!isOperator &&
|
||||
!channel.lists[ExceptMask].Match(client.UserHost()) {
|
||||
!channel.lists[ExceptMask].Match(client.UserHost(false)) {
|
||||
client.ErrBannedFromChan(channel)
|
||||
return
|
||||
}
|
||||
|
||||
client.channels.Add(channel)
|
||||
channel.members.Add(client)
|
||||
if len(channel.members) == 1 {
|
||||
channel.members[client][ChannelCreator] = true
|
||||
channel.members[client][ChannelOperator] = true
|
||||
if channel.members.Count() == 1 {
|
||||
channel.members.Get(client).Set(ChannelCreator)
|
||||
channel.members.Get(client).Set(ChannelOperator)
|
||||
}
|
||||
|
||||
reply := RplJoin(client, channel)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.GetTopic(client)
|
||||
channel.Names(client)
|
||||
}
|
||||
@@ -187,9 +192,10 @@ func (channel *Channel) Part(client *Client, message Text) {
|
||||
}
|
||||
|
||||
reply := RplPart(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.Quit(client)
|
||||
}
|
||||
|
||||
@@ -213,7 +219,7 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
|
||||
return
|
||||
}
|
||||
|
||||
if channel.flags[OpOnlyTopic] && !channel.ClientIsOperator(client) {
|
||||
if channel.flags.Has(OpOnlyTopic) && !channel.ClientIsOperator(client) {
|
||||
client.ErrChanOPrivIsNeeded(channel)
|
||||
return
|
||||
}
|
||||
@@ -221,22 +227,26 @@ func (channel *Channel) SetTopic(client *Client, topic Text) {
|
||||
channel.topic = topic
|
||||
|
||||
reply := RplTopicMsg(client, channel)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) CanSpeak(client *Client) bool {
|
||||
if channel.ClientIsOperator(client) {
|
||||
return true
|
||||
}
|
||||
if channel.flags[NoOutside] && !channel.members.Has(client) {
|
||||
if channel.flags.Has(NoOutside) && !channel.members.Has(client) {
|
||||
return false
|
||||
}
|
||||
if channel.flags[Moderated] && !(channel.members.HasMode(client, Voice) ||
|
||||
if channel.flags.Has(Moderated) && !(channel.members.HasMode(client, Voice) ||
|
||||
channel.members.HasMode(client, ChannelOperator)) {
|
||||
return false
|
||||
}
|
||||
if channel.flags.Has(SecureChan) && !client.modes.Has(SecureConn) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -246,12 +256,14 @@ func (channel *Channel) PrivMsg(client *Client, message Text) {
|
||||
return
|
||||
}
|
||||
reply := RplPrivMsg(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
if member == client {
|
||||
continue
|
||||
return true
|
||||
}
|
||||
client.server.metrics.Counter("client", "messages").Inc()
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
|
||||
@@ -263,17 +275,17 @@ func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode,
|
||||
|
||||
switch op {
|
||||
case Add:
|
||||
if channel.flags[mode] {
|
||||
if channel.flags.Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.flags[mode] = true
|
||||
channel.flags.Set(mode)
|
||||
return true
|
||||
|
||||
case Remove:
|
||||
if !channel.flags[mode] {
|
||||
if !channel.flags.Has(mode) {
|
||||
return false
|
||||
}
|
||||
delete(channel.flags, mode)
|
||||
channel.flags.Unset(mode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -304,17 +316,17 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode,
|
||||
|
||||
switch op {
|
||||
case Add:
|
||||
if channel.members[target][mode] {
|
||||
if channel.members.Get(target).Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.members[target][mode] = true
|
||||
channel.members.Get(target).Set(mode)
|
||||
return true
|
||||
|
||||
case Remove:
|
||||
if !channel.members[target][mode] {
|
||||
if !channel.members.Get(target).Has(mode) {
|
||||
return false
|
||||
}
|
||||
channel.members[target][mode] = false
|
||||
channel.members.Get(target).Unset(mode)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -362,7 +374,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo
|
||||
return channel.applyModeMask(client, change.mode, change.op,
|
||||
NewName(change.arg))
|
||||
|
||||
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private:
|
||||
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private, Secret, SecureChan:
|
||||
return channel.applyModeFlag(client, change.mode, change.op)
|
||||
|
||||
case Key:
|
||||
@@ -428,9 +440,10 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) {
|
||||
|
||||
if len(applied) > 0 {
|
||||
reply := RplChannelMode(client, channel, applied)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,17 +453,21 @@ func (channel *Channel) Notice(client *Client, message Text) {
|
||||
return
|
||||
}
|
||||
reply := RplNotice(client, channel, message)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
if member == client {
|
||||
continue
|
||||
return true
|
||||
}
|
||||
client.server.metrics.Counter("client", "messages").Inc()
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (channel *Channel) Quit(client *Client) {
|
||||
channel.members.Remove(client)
|
||||
client.channels.Remove(channel)
|
||||
// XXX: Race Condition from client.destroy()
|
||||
// Do we need to?
|
||||
// client.channels.Remove(channel)
|
||||
|
||||
if channel.IsEmpty() {
|
||||
channel.server.channels.Remove(channel)
|
||||
@@ -472,14 +489,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment Text) {
|
||||
}
|
||||
|
||||
reply := RplKick(channel, client, target, comment)
|
||||
for member := range channel.members {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
member.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
channel.Quit(target)
|
||||
}
|
||||
|
||||
func (channel *Channel) Invite(invitee *Client, inviter *Client) {
|
||||
if channel.flags[InviteOnly] && !channel.ClientIsOperator(inviter) {
|
||||
if channel.flags.Has(InviteOnly) && !channel.ClientIsOperator(inviter) {
|
||||
inviter.ErrChanOPrivIsNeeded(channel)
|
||||
return
|
||||
}
|
||||
@@ -489,13 +507,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) {
|
||||
return
|
||||
}
|
||||
|
||||
if channel.flags[InviteOnly] {
|
||||
channel.lists[InviteMask].Add(invitee.UserHost())
|
||||
if channel.flags.Has(InviteOnly) {
|
||||
channel.lists[InviteMask].Add(invitee.UserHost(false))
|
||||
}
|
||||
|
||||
inviter.RplInviting(invitee, channel.name)
|
||||
invitee.Reply(RplInviteMsg(inviter, invitee, channel.name))
|
||||
if invitee.flags[Away] {
|
||||
if invitee.modes.Has(Away) {
|
||||
inviter.RplAway(invitee)
|
||||
}
|
||||
}
|
||||
|
310
irc/client.go
310
irc/client.go
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -14,77 +15,118 @@ const (
|
||||
QUIT_TIMEOUT = time.Minute // how long after idle before a client is kicked
|
||||
)
|
||||
|
||||
type SyncBool struct {
|
||||
sync.RWMutex
|
||||
|
||||
value bool
|
||||
}
|
||||
|
||||
func NewSyncBool(value bool) *SyncBool {
|
||||
return &SyncBool{value: value}
|
||||
}
|
||||
|
||||
func (sb *SyncBool) Get() bool {
|
||||
sb.RLock()
|
||||
defer sb.RUnlock()
|
||||
|
||||
return sb.value
|
||||
}
|
||||
|
||||
func (sb *SyncBool) Set(value bool) {
|
||||
sb.Lock()
|
||||
defer sb.Unlock()
|
||||
|
||||
sb.value = value
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
atime time.Time
|
||||
authorized bool
|
||||
awayMessage Text
|
||||
capabilities CapabilitySet
|
||||
capState CapState
|
||||
channels ChannelSet
|
||||
channels *ChannelSet
|
||||
ctime time.Time
|
||||
flags map[UserMode]bool
|
||||
hasQuit bool
|
||||
modes *UserModeSet
|
||||
hasQuit *SyncBool
|
||||
hops uint
|
||||
hostname Name
|
||||
hostmask Name // Cloacked hostname (SHA256)
|
||||
pingTime time.Time
|
||||
idleTimer *time.Timer
|
||||
nick Name
|
||||
quitTimer *time.Timer
|
||||
realname Text
|
||||
registered bool
|
||||
sasl *SaslState
|
||||
server *Server
|
||||
socket *Socket
|
||||
commands chan Command
|
||||
replies chan string
|
||||
username Name
|
||||
}
|
||||
|
||||
func NewClient(server *Server, conn net.Conn) *Client {
|
||||
now := time.Now()
|
||||
client := &Client{
|
||||
c := &Client{
|
||||
atime: now,
|
||||
authorized: len(server.password) == 0,
|
||||
capState: CapNone,
|
||||
capabilities: make(CapabilitySet),
|
||||
channels: make(ChannelSet),
|
||||
channels: NewChannelSet(),
|
||||
ctime: now,
|
||||
flags: make(map[UserMode]bool),
|
||||
modes: NewUserModeSet(),
|
||||
hasQuit: NewSyncBool(false),
|
||||
sasl: NewSaslState(),
|
||||
server: server,
|
||||
socket: NewSocket(conn),
|
||||
commands: make(chan Command),
|
||||
replies: make(chan string),
|
||||
}
|
||||
|
||||
if _, ok := conn.(*tls.Conn); ok {
|
||||
client.flags[SecureConn] = true
|
||||
client.flags[SecureOnly] = true
|
||||
c.modes.Set(SecureConn)
|
||||
}
|
||||
|
||||
client.Touch()
|
||||
go client.run()
|
||||
c.Touch()
|
||||
go c.writeloop()
|
||||
go c.readloop()
|
||||
|
||||
return client
|
||||
return c
|
||||
}
|
||||
|
||||
//
|
||||
// command goroutine
|
||||
//
|
||||
|
||||
func (client *Client) run() {
|
||||
func (c *Client) writeloop() {
|
||||
for {
|
||||
select {
|
||||
case reply, ok := <-c.replies:
|
||||
if !ok || reply == "" || c.socket == nil {
|
||||
return
|
||||
}
|
||||
c.socket.Write(reply)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readloop() {
|
||||
var command Command
|
||||
var err error
|
||||
var line string
|
||||
|
||||
// Set the hostname for this client.
|
||||
client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr())
|
||||
c.hostname = AddrLookupHostname(c.socket.conn.RemoteAddr())
|
||||
c.hostmask = NewName(SHA256(c.hostname.String()))
|
||||
|
||||
for err == nil {
|
||||
if line, err = client.socket.Read(); err != nil {
|
||||
if line, err = c.socket.Read(); err != nil {
|
||||
command = NewQuitCommand("connection closed")
|
||||
|
||||
} else if command, err = ParseCommand(line); err != nil {
|
||||
switch err {
|
||||
case ErrParseCommand:
|
||||
//TODO(dan): use the real failed numeric for this (400)
|
||||
client.Reply(RplNotice(client.server, client,
|
||||
NewText("failed to parse command")))
|
||||
c.Reply(RplNotice(c.server, c, NewText("failed to parse command")))
|
||||
|
||||
case NotEnoughArgsError:
|
||||
// TODO
|
||||
@@ -94,7 +136,7 @@ func (client *Client) run() {
|
||||
continue
|
||||
|
||||
} else if checkPass, ok := command.(checkPasswordCommand); ok {
|
||||
checkPass.LoadPassword(client.server)
|
||||
checkPass.LoadPassword(c.server)
|
||||
// Block the client thread while handling a potentially expensive
|
||||
// password bcrypt operation. Since the server is single-threaded
|
||||
// for commands, we don't want the server to perform the bcrypt,
|
||||
@@ -103,168 +145,182 @@ func (client *Client) run() {
|
||||
checkPass.CheckPassword()
|
||||
}
|
||||
|
||||
client.processCommand(command)
|
||||
c.processCommand(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) processCommand(cmd Command) {
|
||||
cmd.SetClient(client)
|
||||
func (c *Client) processCommand(cmd Command) {
|
||||
cmd.SetClient(c)
|
||||
|
||||
if !client.registered {
|
||||
if !c.registered {
|
||||
regCmd, ok := cmd.(RegServerCommand)
|
||||
if !ok {
|
||||
client.Quit("unexpected command")
|
||||
c.Quit("unexpected command")
|
||||
return
|
||||
}
|
||||
regCmd.HandleRegServer(client.server)
|
||||
regCmd.HandleRegServer(c.server)
|
||||
return
|
||||
}
|
||||
|
||||
srvCmd, ok := cmd.(ServerCommand)
|
||||
if !ok {
|
||||
client.ErrUnknownCommand(cmd.Code())
|
||||
c.ErrUnknownCommand(cmd.Code())
|
||||
return
|
||||
}
|
||||
|
||||
c.server.metrics.Counter("client", "commands").Inc()
|
||||
|
||||
defer func(t time.Time) {
|
||||
v := c.server.metrics.SummaryVec("client", "command_duration_seconds")
|
||||
v.WithLabelValues(cmd.Code().String()).Observe(time.Now().Sub(t).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
switch srvCmd.(type) {
|
||||
case *PingCommand, *PongCommand:
|
||||
client.Touch()
|
||||
c.Touch()
|
||||
|
||||
case *QuitCommand:
|
||||
// no-op
|
||||
|
||||
default:
|
||||
client.Active()
|
||||
client.Touch()
|
||||
c.Active()
|
||||
c.Touch()
|
||||
}
|
||||
|
||||
srvCmd.HandleServer(client.server)
|
||||
srvCmd.HandleServer(c.server)
|
||||
}
|
||||
|
||||
// quit timer goroutine
|
||||
|
||||
func (client *Client) connectionTimeout() {
|
||||
client.processCommand(NewQuitCommand("connection timeout"))
|
||||
func (c *Client) connectionTimeout() {
|
||||
c.processCommand(NewQuitCommand("connection timeout"))
|
||||
}
|
||||
|
||||
//
|
||||
// idle timer goroutine
|
||||
//
|
||||
|
||||
func (client *Client) connectionIdle() {
|
||||
client.server.idle <- client
|
||||
func (c *Client) connectionIdle() {
|
||||
c.server.idle <- c
|
||||
}
|
||||
|
||||
//
|
||||
// server goroutine
|
||||
//
|
||||
|
||||
func (client *Client) Active() {
|
||||
client.atime = time.Now()
|
||||
func (c *Client) Active() {
|
||||
c.atime = time.Now()
|
||||
}
|
||||
|
||||
func (client *Client) Touch() {
|
||||
if client.quitTimer != nil {
|
||||
client.quitTimer.Stop()
|
||||
func (c *Client) Touch() {
|
||||
if c.quitTimer != nil {
|
||||
c.quitTimer.Stop()
|
||||
}
|
||||
|
||||
if client.idleTimer == nil {
|
||||
client.idleTimer = time.AfterFunc(IDLE_TIMEOUT, client.connectionIdle)
|
||||
if c.idleTimer == nil {
|
||||
c.idleTimer = time.AfterFunc(IDLE_TIMEOUT, c.connectionIdle)
|
||||
} else {
|
||||
client.idleTimer.Reset(IDLE_TIMEOUT)
|
||||
c.idleTimer.Reset(IDLE_TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Idle() {
|
||||
client.Reply(RplPing(client.server))
|
||||
func (c *Client) Idle() {
|
||||
c.pingTime = time.Now()
|
||||
c.Reply(RplPing(c.server))
|
||||
|
||||
if client.quitTimer == nil {
|
||||
client.quitTimer = time.AfterFunc(QUIT_TIMEOUT, client.connectionTimeout)
|
||||
if c.quitTimer == nil {
|
||||
c.quitTimer = time.AfterFunc(QUIT_TIMEOUT, c.connectionTimeout)
|
||||
} else {
|
||||
client.quitTimer.Reset(QUIT_TIMEOUT)
|
||||
c.quitTimer.Reset(QUIT_TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Register() {
|
||||
if client.registered {
|
||||
func (c *Client) Register() {
|
||||
if c.registered {
|
||||
return
|
||||
}
|
||||
client.registered = true
|
||||
client.Touch()
|
||||
c.registered = true
|
||||
c.modes.Set(HostMask)
|
||||
c.Touch()
|
||||
}
|
||||
|
||||
func (client *Client) destroy() {
|
||||
func (c *Client) destroy() {
|
||||
// clean up channels
|
||||
|
||||
for channel := range client.channels {
|
||||
channel.Quit(client)
|
||||
}
|
||||
c.channels.Range(func(channel *Channel) bool {
|
||||
channel.Quit(c)
|
||||
return true
|
||||
})
|
||||
|
||||
// clean up server
|
||||
|
||||
client.server.connections -= 1
|
||||
client.server.clients.Remove(client)
|
||||
if _, ok := c.socket.conn.(*tls.Conn); ok {
|
||||
c.server.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Dec()
|
||||
} else {
|
||||
c.server.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Dec()
|
||||
}
|
||||
|
||||
c.server.connections.Dec()
|
||||
c.server.clients.Remove(c)
|
||||
|
||||
// clean up self
|
||||
|
||||
if client.idleTimer != nil {
|
||||
client.idleTimer.Stop()
|
||||
if c.idleTimer != nil {
|
||||
c.idleTimer.Stop()
|
||||
}
|
||||
if client.quitTimer != nil {
|
||||
client.quitTimer.Stop()
|
||||
if c.quitTimer != nil {
|
||||
c.quitTimer.Stop()
|
||||
}
|
||||
|
||||
client.socket.Close()
|
||||
close(c.replies)
|
||||
|
||||
log.Debugf("%s: destroyed", client)
|
||||
c.socket.Close()
|
||||
|
||||
log.Debugf("%s: destroyed", c)
|
||||
}
|
||||
|
||||
func (client *Client) IdleTime() time.Duration {
|
||||
return time.Since(client.atime)
|
||||
func (c *Client) IdleTime() time.Duration {
|
||||
return time.Since(c.atime)
|
||||
}
|
||||
|
||||
func (client *Client) SignonTime() int64 {
|
||||
return client.ctime.Unix()
|
||||
func (c *Client) SignonTime() int64 {
|
||||
return c.ctime.Unix()
|
||||
}
|
||||
|
||||
func (client *Client) IdleSeconds() uint64 {
|
||||
return uint64(client.IdleTime().Seconds())
|
||||
func (c *Client) IdleSeconds() uint64 {
|
||||
return uint64(c.IdleTime().Seconds())
|
||||
}
|
||||
|
||||
func (client *Client) HasNick() bool {
|
||||
return client.nick != ""
|
||||
func (c *Client) HasNick() bool {
|
||||
return c.nick != ""
|
||||
}
|
||||
|
||||
func (client *Client) HasUsername() bool {
|
||||
return client.username != ""
|
||||
func (c *Client) HasUsername() bool {
|
||||
return c.username != ""
|
||||
}
|
||||
|
||||
func (client *Client) CanSpeak(target *Client) bool {
|
||||
requiresSecure := client.flags[SecureOnly] || target.flags[SecureOnly]
|
||||
isSecure := client.flags[SecureConn] && target.flags[SecureConn]
|
||||
isOperator := client.flags[Operator]
|
||||
func (c *Client) CanSpeak(target *Client) bool {
|
||||
requiresSecure := c.modes.Has(SecureOnly) || target.modes.Has(SecureOnly)
|
||||
isSecure := c.modes.Has(SecureConn) && target.modes.Has(SecureConn)
|
||||
isOperator := c.modes.Has(Operator)
|
||||
|
||||
return !requiresSecure || (requiresSecure && (isOperator || isSecure))
|
||||
}
|
||||
|
||||
// <mode>
|
||||
func (c *Client) ModeString() (str string) {
|
||||
for flag := range c.flags {
|
||||
str += flag.String()
|
||||
}
|
||||
|
||||
if len(str) > 0 {
|
||||
str = "+" + str
|
||||
}
|
||||
return
|
||||
return c.modes.String()
|
||||
}
|
||||
|
||||
func (c *Client) UserHost() Name {
|
||||
func (c *Client) UserHost(cloacked bool) Name {
|
||||
username := "*"
|
||||
if c.HasUsername() {
|
||||
if c.username != "" {
|
||||
username = c.username.String()
|
||||
}
|
||||
return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname))
|
||||
if cloacked {
|
||||
return Name(fmt.Sprintf("%s!%s@%s", c.nick, username, c.hostmask))
|
||||
}
|
||||
return Name(fmt.Sprintf("%s!%s@%s", c.nick, username, c.hostname))
|
||||
}
|
||||
|
||||
func (c *Client) Server() Name {
|
||||
@@ -283,65 +339,71 @@ func (c *Client) Nick() Name {
|
||||
}
|
||||
|
||||
func (c *Client) Id() Name {
|
||||
return c.UserHost()
|
||||
return c.UserHost(true)
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
return c.Id().String()
|
||||
}
|
||||
|
||||
func (client *Client) Friends() ClientSet {
|
||||
friends := make(ClientSet)
|
||||
friends.Add(client)
|
||||
for channel := range client.channels {
|
||||
for member := range channel.members {
|
||||
func (c *Client) Friends() *ClientSet {
|
||||
friends := NewClientSet()
|
||||
friends.Add(c)
|
||||
c.channels.Range(func(channel *Channel) bool {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
friends.Add(member)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
return friends
|
||||
}
|
||||
|
||||
func (client *Client) SetNickname(nickname Name) {
|
||||
if client.HasNick() {
|
||||
log.Errorf("%s nickname already set!", client)
|
||||
func (c *Client) SetNickname(nickname Name) {
|
||||
if c.nick != "" {
|
||||
log.Errorf("%s nickname already set!", c)
|
||||
return
|
||||
}
|
||||
client.nick = nickname
|
||||
client.server.clients.Add(client)
|
||||
c.nick = nickname
|
||||
c.server.clients.Add(c)
|
||||
}
|
||||
|
||||
func (client *Client) ChangeNickname(nickname Name) {
|
||||
func (c *Client) ChangeNickname(nickname Name) {
|
||||
// Make reply before changing nick to capture original source id.
|
||||
reply := RplNick(client, nickname)
|
||||
client.server.clients.Remove(client)
|
||||
client.server.whoWas.Append(client)
|
||||
client.nick = nickname
|
||||
client.server.clients.Add(client)
|
||||
for friend := range client.Friends() {
|
||||
reply := RplNick(c, nickname)
|
||||
c.server.clients.Remove(c)
|
||||
c.server.whoWas.Append(c)
|
||||
c.nick = nickname
|
||||
c.server.clients.Add(c)
|
||||
c.Friends().Range(func(friend *Client) bool {
|
||||
friend.Reply(reply)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) Reply(reply string) {
|
||||
if !c.hasQuit.Get() {
|
||||
c.replies <- reply
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) Reply(reply string) error {
|
||||
return client.socket.Write(reply)
|
||||
}
|
||||
|
||||
func (client *Client) Quit(message Text) {
|
||||
if client.hasQuit {
|
||||
func (c *Client) Quit(message Text) {
|
||||
if c.hasQuit.Get() {
|
||||
return
|
||||
}
|
||||
|
||||
client.hasQuit = true
|
||||
client.Reply(RplError("quit"))
|
||||
client.server.whoWas.Append(client)
|
||||
friends := client.Friends()
|
||||
friends.Remove(client)
|
||||
client.destroy()
|
||||
c.hasQuit.Set(true)
|
||||
c.Reply(RplError("quit"))
|
||||
c.server.whoWas.Append(c)
|
||||
friends := c.Friends()
|
||||
friends.Remove(c)
|
||||
c.destroy()
|
||||
|
||||
if len(friends) > 0 {
|
||||
reply := RplQuit(client, message)
|
||||
for friend := range friends {
|
||||
if friends.Count() > 0 {
|
||||
reply := RplQuit(c, message)
|
||||
friends.Range(func(friend *Client) bool {
|
||||
friend.Reply(reply)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -29,12 +29,12 @@ func ExpandUserHost(userhost Name) (expanded Name) {
|
||||
|
||||
type ClientLookupSet struct {
|
||||
sync.RWMutex
|
||||
byNick map[Name]*Client
|
||||
nicks map[Name]*Client
|
||||
}
|
||||
|
||||
func NewClientLookupSet() *ClientLookupSet {
|
||||
return &ClientLookupSet{
|
||||
byNick: make(map[Name]*Client),
|
||||
nicks: make(map[Name]*Client),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ func (clients *ClientLookupSet) Count() int {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
return len(clients.byNick)
|
||||
return len(clients.nicks)
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Get(nick Name) *Client {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
return clients.byNick[nick.ToLower()]
|
||||
return clients.nicks[nick.ToLower()]
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) Add(client *Client) error {
|
||||
@@ -63,7 +63,7 @@ func (clients *ClientLookupSet) Add(client *Client) error {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
clients.byNick[client.Nick().ToLower()] = client
|
||||
clients.nicks[client.Nick().ToLower()] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,22 +78,32 @@ func (clients *ClientLookupSet) Remove(client *Client) error {
|
||||
clients.Lock()
|
||||
defer clients.Unlock()
|
||||
|
||||
delete(clients.byNick, client.nick.ToLower())
|
||||
delete(clients.nicks, client.nick.ToLower())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) FindAll(userhost Name) (set ClientSet) {
|
||||
func (clients *ClientLookupSet) Range(f func(nick Name, client *Client) bool) {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
for nick, client := range clients.nicks {
|
||||
if !f(nick, client) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
|
||||
clients.RLock()
|
||||
defer clients.RUnlock()
|
||||
|
||||
set = make(ClientSet)
|
||||
set := NewClientSet()
|
||||
|
||||
userhost = ExpandUserHost(userhost)
|
||||
matcher := ircmatch.MakeMatch(userhost.String())
|
||||
|
||||
var casemappedNickMask string
|
||||
for _, client := range clients.byNick {
|
||||
casemappedNickMask = client.UserHost().String()
|
||||
for _, client := range clients.nicks {
|
||||
casemappedNickMask = client.UserHost(false).String()
|
||||
if matcher.Match(casemappedNickMask) {
|
||||
set.Add(client)
|
||||
}
|
||||
@@ -110,8 +120,8 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client {
|
||||
matcher := ircmatch.MakeMatch(userhost.String())
|
||||
|
||||
var casemappedNickMask string
|
||||
for _, client := range clients.byNick {
|
||||
casemappedNickMask = client.UserHost().String()
|
||||
for _, client := range clients.nicks {
|
||||
casemappedNickMask = client.UserHost(false).String()
|
||||
if matcher.Match(casemappedNickMask) {
|
||||
return client
|
||||
}
|
||||
|
@@ -26,36 +26,38 @@ var (
|
||||
NotEnoughArgsError = errors.New("not enough arguments")
|
||||
ErrParseCommand = errors.New("failed to parse message")
|
||||
parseCommandFuncs = map[StringCode]parseCommandFunc{
|
||||
AWAY: ParseAwayCommand,
|
||||
CAP: ParseCapCommand,
|
||||
INVITE: ParseInviteCommand,
|
||||
ISON: ParseIsOnCommand,
|
||||
JOIN: ParseJoinCommand,
|
||||
KICK: ParseKickCommand,
|
||||
KILL: ParseKillCommand,
|
||||
LIST: ParseListCommand,
|
||||
MODE: ParseModeCommand,
|
||||
MOTD: ParseMOTDCommand,
|
||||
NAMES: ParseNamesCommand,
|
||||
NICK: ParseNickCommand,
|
||||
NOTICE: ParseNoticeCommand,
|
||||
ONICK: ParseOperNickCommand,
|
||||
OPER: ParseOperCommand,
|
||||
REHASH: ParseRehashCommand,
|
||||
PART: ParsePartCommand,
|
||||
PASS: ParsePassCommand,
|
||||
PING: ParsePingCommand,
|
||||
PONG: ParsePongCommand,
|
||||
PRIVMSG: ParsePrivMsgCommand,
|
||||
QUIT: ParseQuitCommand,
|
||||
TIME: ParseTimeCommand,
|
||||
LUSERS: ParseLUsersCommand,
|
||||
TOPIC: ParseTopicCommand,
|
||||
USER: ParseUserCommand,
|
||||
VERSION: ParseVersionCommand,
|
||||
WHO: ParseWhoCommand,
|
||||
WHOIS: ParseWhoisCommand,
|
||||
WHOWAS: ParseWhoWasCommand,
|
||||
AUTHENTICATE: ParseAuthenticateCommand,
|
||||
AWAY: ParseAwayCommand,
|
||||
CAP: ParseCapCommand,
|
||||
INVITE: ParseInviteCommand,
|
||||
ISON: ParseIsOnCommand,
|
||||
JOIN: ParseJoinCommand,
|
||||
KICK: ParseKickCommand,
|
||||
KILL: ParseKillCommand,
|
||||
LIST: ParseListCommand,
|
||||
MODE: ParseModeCommand,
|
||||
MOTD: ParseMOTDCommand,
|
||||
NAMES: ParseNamesCommand,
|
||||
NICK: ParseNickCommand,
|
||||
NOTICE: ParseNoticeCommand,
|
||||
ONICK: ParseOperNickCommand,
|
||||
OPER: ParseOperCommand,
|
||||
REHASH: ParseRehashCommand,
|
||||
PART: ParsePartCommand,
|
||||
PASS: ParsePassCommand,
|
||||
PING: ParsePingCommand,
|
||||
PONG: ParsePongCommand,
|
||||
PRIVMSG: ParsePrivMsgCommand,
|
||||
QUIT: ParseQuitCommand,
|
||||
TIME: ParseTimeCommand,
|
||||
LUSERS: ParseLUsersCommand,
|
||||
TOPIC: ParseTopicCommand,
|
||||
USER: ParseUserCommand,
|
||||
VERSION: ParseVersionCommand,
|
||||
WALLOPS: ParseWallopsCommand,
|
||||
WHO: ParseWhoCommand,
|
||||
WHOIS: ParseWhoisCommand,
|
||||
WHOWAS: ParseWhoWasCommand,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -182,6 +184,22 @@ func ParsePongCommand(args []string) (Command, error) {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
// AUTHENTICATE <arg>
|
||||
|
||||
type AuthenticateCommand struct {
|
||||
BaseCommand
|
||||
arg string
|
||||
}
|
||||
|
||||
func ParseAuthenticateCommand(args []string) (Command, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, NotEnoughArgsError
|
||||
}
|
||||
return &AuthenticateCommand{
|
||||
arg: args[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PASS <password>
|
||||
|
||||
type PassCommand struct {
|
||||
@@ -875,6 +893,20 @@ func ParseKillCommand(args []string) (Command, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WallopsCommand struct {
|
||||
BaseCommand
|
||||
message Text
|
||||
}
|
||||
|
||||
func ParseWallopsCommand(args []string) (Command, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, NotEnoughArgsError
|
||||
}
|
||||
return &WallopsCommand{
|
||||
message: NewText(args[0]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WhoWasCommand struct {
|
||||
BaseCommand
|
||||
nicknames []Name
|
||||
|
@@ -31,6 +31,10 @@ type Config struct {
|
||||
sync.Mutex
|
||||
filename string
|
||||
|
||||
Network struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
Server struct {
|
||||
PassConfig `yaml:",inline"`
|
||||
Listen []string
|
||||
@@ -42,6 +46,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
Operator map[string]*PassConfig
|
||||
Account map[string]*PassConfig
|
||||
}
|
||||
|
||||
func (conf *Config) Operators() map[Name][]byte {
|
||||
@@ -52,6 +57,14 @@ func (conf *Config) Operators() map[Name][]byte {
|
||||
return operators
|
||||
}
|
||||
|
||||
func (conf *Config) Accounts() map[string][]byte {
|
||||
accounts := make(map[string][]byte)
|
||||
for name, account := range conf.Account {
|
||||
accounts[name] = []byte(account.Password)
|
||||
}
|
||||
return accounts
|
||||
}
|
||||
|
||||
func (conf *Config) Name() string {
|
||||
return conf.filename
|
||||
}
|
||||
@@ -86,6 +99,10 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
|
||||
config.filename = filename
|
||||
|
||||
if config.Network.Name == "" {
|
||||
return nil, errors.New("Network name missing")
|
||||
}
|
||||
|
||||
if config.Server.Name == "" {
|
||||
return nil, errors.New("Server name missing")
|
||||
}
|
||||
|
@@ -5,37 +5,39 @@ const (
|
||||
MAX_REPLY_LEN = 512 - len(CRLF)
|
||||
|
||||
// string codes
|
||||
AWAY StringCode = "AWAY"
|
||||
CAP StringCode = "CAP"
|
||||
ERROR StringCode = "ERROR"
|
||||
INVITE StringCode = "INVITE"
|
||||
ISON StringCode = "ISON"
|
||||
JOIN StringCode = "JOIN"
|
||||
KICK StringCode = "KICK"
|
||||
KILL StringCode = "KILL"
|
||||
LIST StringCode = "LIST"
|
||||
MODE StringCode = "MODE"
|
||||
MOTD StringCode = "MOTD"
|
||||
NAMES StringCode = "NAMES"
|
||||
NICK StringCode = "NICK"
|
||||
NOTICE StringCode = "NOTICE"
|
||||
ONICK StringCode = "ONICK"
|
||||
OPER StringCode = "OPER"
|
||||
REHASH StringCode = "REHASH"
|
||||
PART StringCode = "PART"
|
||||
PASS StringCode = "PASS"
|
||||
PING StringCode = "PING"
|
||||
PONG StringCode = "PONG"
|
||||
PRIVMSG StringCode = "PRIVMSG"
|
||||
QUIT StringCode = "QUIT"
|
||||
TIME StringCode = "TIME"
|
||||
LUSERS StringCode = "LUSERS"
|
||||
TOPIC StringCode = "TOPIC"
|
||||
USER StringCode = "USER"
|
||||
VERSION StringCode = "VERSION"
|
||||
WHO StringCode = "WHO"
|
||||
WHOIS StringCode = "WHOIS"
|
||||
WHOWAS StringCode = "WHOWAS"
|
||||
AUTHENTICATE StringCode = "AUTHENTICATE" // SASL
|
||||
AWAY StringCode = "AWAY"
|
||||
CAP StringCode = "CAP"
|
||||
ERROR StringCode = "ERROR"
|
||||
INVITE StringCode = "INVITE"
|
||||
ISON StringCode = "ISON"
|
||||
JOIN StringCode = "JOIN"
|
||||
KICK StringCode = "KICK"
|
||||
KILL StringCode = "KILL"
|
||||
LIST StringCode = "LIST"
|
||||
MODE StringCode = "MODE"
|
||||
MOTD StringCode = "MOTD"
|
||||
NAMES StringCode = "NAMES"
|
||||
NICK StringCode = "NICK"
|
||||
NOTICE StringCode = "NOTICE"
|
||||
ONICK StringCode = "ONICK"
|
||||
OPER StringCode = "OPER"
|
||||
REHASH StringCode = "REHASH"
|
||||
PART StringCode = "PART"
|
||||
PASS StringCode = "PASS"
|
||||
PING StringCode = "PING"
|
||||
PONG StringCode = "PONG"
|
||||
PRIVMSG StringCode = "PRIVMSG"
|
||||
QUIT StringCode = "QUIT"
|
||||
TIME StringCode = "TIME"
|
||||
LUSERS StringCode = "LUSERS"
|
||||
TOPIC StringCode = "TOPIC"
|
||||
USER StringCode = "USER"
|
||||
VERSION StringCode = "VERSION"
|
||||
WALLOPS StringCode = "WALLOPS"
|
||||
WHO StringCode = "WHO"
|
||||
WHOIS StringCode = "WHOIS"
|
||||
WHOWAS StringCode = "WHOWAS"
|
||||
|
||||
// numeric codes
|
||||
RPL_WELCOME NumericCode = 1
|
||||
@@ -91,6 +93,7 @@ const (
|
||||
RPL_LISTEND NumericCode = 323
|
||||
RPL_CHANNELMODEIS NumericCode = 324
|
||||
RPL_UNIQOPIS NumericCode = 325
|
||||
RPL_WHOISLOGGEDIN NumericCode = 330
|
||||
RPL_NOTOPIC NumericCode = 331
|
||||
RPL_TOPIC NumericCode = 332
|
||||
RPL_INVITING NumericCode = 341
|
||||
@@ -177,4 +180,15 @@ const (
|
||||
ERR_UMODEUNKNOWNFLAG NumericCode = 501
|
||||
ERR_USERSDONTMATCH NumericCode = 502
|
||||
RPL_WHOISSECURE NumericCode = 671
|
||||
|
||||
// SASL
|
||||
RPL_LOGGEDIN NumericCode = 900
|
||||
RPL_LOGGEDOUT NumericCode = 901
|
||||
ERR_NICKLOCKED NumericCode = 902
|
||||
RPL_SASLSUCCESS NumericCode = 903
|
||||
ERR_SASLFAIL NumericCode = 904
|
||||
ERR_SASLTOOLONG NumericCode = 905
|
||||
ERR_SASLABORTED NumericCode = 906
|
||||
ERR_SASLALREADY NumericCode = 907
|
||||
RPL_SASLMECHS NumericCode = 908
|
||||
)
|
||||
|
236
irc/metrics.go
Normal file
236
irc/metrics.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// DefObjectives ...
|
||||
var DefObjectives = map[float64]float64{
|
||||
0.50: 0.05,
|
||||
0.90: 0.01,
|
||||
0.95: 0.005,
|
||||
0.99: 0.001,
|
||||
}
|
||||
|
||||
// Metrics ...
|
||||
type Metrics struct {
|
||||
sync.RWMutex
|
||||
|
||||
namespace string
|
||||
metrics map[string]prometheus.Metric
|
||||
guagevecs map[string]*prometheus.GaugeVec
|
||||
sumvecs map[string]*prometheus.SummaryVec
|
||||
}
|
||||
|
||||
// NewMetrics ...
|
||||
func NewMetrics(namespace string) *Metrics {
|
||||
return &Metrics{
|
||||
namespace: namespace,
|
||||
metrics: make(map[string]prometheus.Metric),
|
||||
guagevecs: make(map[string]*prometheus.GaugeVec),
|
||||
sumvecs: make(map[string]*prometheus.SummaryVec),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCounter ...
|
||||
func (m *Metrics) NewCounter(subsystem, name, help string) prometheus.Counter {
|
||||
counter := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.metrics[key] = counter
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(counter)
|
||||
|
||||
return counter
|
||||
}
|
||||
|
||||
// NewCounterFunc ...
|
||||
func (m *Metrics) NewCounterFunc(subsystem, name, help string, f func() float64) prometheus.CounterFunc {
|
||||
counter := prometheus.NewCounterFunc(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.metrics[key] = counter
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(counter)
|
||||
|
||||
return counter
|
||||
}
|
||||
|
||||
// NewGauge ...
|
||||
func (m *Metrics) NewGauge(subsystem, name, help string) prometheus.Gauge {
|
||||
guage := prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.metrics[key] = guage
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(guage)
|
||||
|
||||
return guage
|
||||
}
|
||||
|
||||
// NewGaugeFunc ...
|
||||
func (m *Metrics) NewGaugeFunc(subsystem, name, help string, f func() float64) prometheus.GaugeFunc {
|
||||
guage := prometheus.NewGaugeFunc(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.metrics[key] = guage
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(guage)
|
||||
|
||||
return guage
|
||||
}
|
||||
|
||||
// NewGaugeVec ...
|
||||
func (m *Metrics) NewGaugeVec(subsystem, name, help string, labels []string) *prometheus.GaugeVec {
|
||||
guagevec := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
labels,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.guagevecs[key] = guagevec
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(guagevec)
|
||||
|
||||
return guagevec
|
||||
}
|
||||
|
||||
// NewSummary ...
|
||||
func (m *Metrics) NewSummary(subsystem, name, help string) prometheus.Summary {
|
||||
summary := prometheus.NewSummary(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
Objectives: DefObjectives,
|
||||
},
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.metrics[key] = summary
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(summary)
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// NewSummaryVec ...
|
||||
func (m *Metrics) NewSummaryVec(subsystem, name, help string, labels []string) *prometheus.SummaryVec {
|
||||
sumvec := prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: m.namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: name,
|
||||
Help: help,
|
||||
Objectives: DefObjectives,
|
||||
},
|
||||
labels,
|
||||
)
|
||||
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.Lock()
|
||||
m.sumvecs[key] = sumvec
|
||||
m.Unlock()
|
||||
prometheus.MustRegister(sumvec)
|
||||
|
||||
return sumvec
|
||||
}
|
||||
|
||||
// Counter ...
|
||||
func (m *Metrics) Counter(subsystem, name string) prometheus.Counter {
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
return m.metrics[key].(prometheus.Counter)
|
||||
}
|
||||
|
||||
// Gauge ...
|
||||
func (m *Metrics) Gauge(subsystem, name string) prometheus.Gauge {
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.metrics[key].(prometheus.Gauge)
|
||||
}
|
||||
|
||||
// GaugeVec ...
|
||||
func (m *Metrics) GaugeVec(subsystem, name string) *prometheus.GaugeVec {
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.guagevecs[key]
|
||||
}
|
||||
|
||||
// Summary ...
|
||||
func (m *Metrics) Summary(subsystem, name string) prometheus.Summary {
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.metrics[key].(prometheus.Summary)
|
||||
}
|
||||
|
||||
// SummaryVec ...
|
||||
func (m *Metrics) SummaryVec(subsystem, name string) *prometheus.SummaryVec {
|
||||
key := fmt.Sprintf("%s_%s", subsystem, name)
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return m.sumvecs[key]
|
||||
}
|
||||
|
||||
// Handler ...
|
||||
func (m *Metrics) Handler() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
||||
|
||||
// Run ...
|
||||
func (m *Metrics) Run(addr string) {
|
||||
http.Handle("/", m.Handler())
|
||||
log.Infof("metrics endpoint listening on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, nil))
|
||||
}
|
51
irc/metrics_test.go
Normal file
51
irc/metrics_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
m := NewMetrics("test")
|
||||
m.NewCounter("foo", "counter", "help")
|
||||
m.NewCounterFunc("foo", "counter_func", "help", func() float64 { return 1.0 })
|
||||
m.NewGauge("foo", "gauge", "help")
|
||||
m.NewGaugeFunc("foo", "gauge_func", "help", func() float64 { return 1.0 })
|
||||
m.NewGaugeVec("foo", "gauge_vec", "help", []string{"test"})
|
||||
|
||||
m.Counter("foo", "counter").Inc()
|
||||
m.Gauge("foo", "gauge").Add(1)
|
||||
m.GaugeVec("foo", "gauge_vec").WithLabelValues("test").Add(1)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("GET", "/", nil)
|
||||
|
||||
m.Handler().ServeHTTP(w, r)
|
||||
assert.Equal(w.Code, http.StatusOK)
|
||||
|
||||
assert.Regexp(
|
||||
`
|
||||
# HELP test_foo_counter help
|
||||
# TYPE test_foo_counter counter
|
||||
test_foo_counter 1
|
||||
# HELP test_foo_counter_func help
|
||||
# TYPE test_foo_counter_func counter
|
||||
test_foo_counter_func 1
|
||||
# HELP test_foo_gauge help
|
||||
# TYPE test_foo_gauge gauge
|
||||
test_foo_gauge 1
|
||||
# HELP test_foo_gauge_func help
|
||||
# TYPE test_foo_gauge_func gauge
|
||||
test_foo_gauge_func 1
|
||||
# HELP test_foo_gauge_vec help
|
||||
# TYPE test_foo_gauge_vec gauge
|
||||
test_foo_gauge_vec{test="test"} 1
|
||||
`,
|
||||
w.Body.String(),
|
||||
)
|
||||
}
|
45
irc/modes.go
45
irc/modes.go
@@ -51,20 +51,19 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
Away UserMode = 'a' // not a real user mode (flag)
|
||||
Invisible UserMode = 'i'
|
||||
LocalOperator UserMode = 'O'
|
||||
Operator UserMode = 'o'
|
||||
Restricted UserMode = 'r'
|
||||
ServerNotice UserMode = 's' // deprecated
|
||||
WallOps UserMode = 'w'
|
||||
SecureConn UserMode = 'z'
|
||||
SecureOnly UserMode = 'Z'
|
||||
Away UserMode = 'a' // not a real user mode (flag)
|
||||
Invisible UserMode = 'i'
|
||||
Operator UserMode = 'o'
|
||||
WallOps UserMode = 'w'
|
||||
Registered UserMode = 'r' // not a real user mode (flag)
|
||||
SecureConn UserMode = 'z'
|
||||
SecureOnly UserMode = 'Z'
|
||||
HostMask UserMode = 'x'
|
||||
)
|
||||
|
||||
var (
|
||||
SupportedUserModes = UserModes{
|
||||
Invisible, Operator,
|
||||
Invisible, Operator, HostMask,
|
||||
}
|
||||
DefaultChannelModes = ChannelModes{
|
||||
NoOutside, OpOnlyTopic,
|
||||
@@ -72,7 +71,6 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
Anonymous ChannelMode = 'a' // flag
|
||||
BanMask ChannelMode = 'b' // arg
|
||||
ChannelCreator ChannelMode = 'O' // flag
|
||||
ChannelOperator ChannelMode = 'o' // arg
|
||||
@@ -84,17 +82,16 @@ const (
|
||||
NoOutside ChannelMode = 'n' // flag
|
||||
OpOnlyTopic ChannelMode = 't' // flag
|
||||
Private ChannelMode = 'p' // flag
|
||||
Quiet ChannelMode = 'q' // flag
|
||||
ReOp ChannelMode = 'r' // flag
|
||||
Secret ChannelMode = 's' // flag, deprecated
|
||||
UserLimit ChannelMode = 'l' // flag arg
|
||||
Voice ChannelMode = 'v' // arg
|
||||
SecureChan ChannelMode = 'Z' // arg
|
||||
)
|
||||
|
||||
var (
|
||||
SupportedChannelModes = ChannelModes{
|
||||
BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside,
|
||||
OpOnlyTopic, Private, UserLimit,
|
||||
OpOnlyTopic, Private, UserLimit, Secret, SecureChan,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -111,7 +108,7 @@ func (m *ModeCommand) HandleServer(s *Server) {
|
||||
return
|
||||
}
|
||||
|
||||
if client != target && !client.flags[Operator] {
|
||||
if client != target && !client.modes.Has(Operator) {
|
||||
client.ErrUsersDontMatch()
|
||||
return
|
||||
}
|
||||
@@ -120,29 +117,27 @@ func (m *ModeCommand) HandleServer(s *Server) {
|
||||
|
||||
for _, change := range m.changes {
|
||||
switch change.mode {
|
||||
case Invisible, ServerNotice, WallOps, SecureOnly:
|
||||
case Invisible, HostMask, WallOps, SecureOnly:
|
||||
switch change.op {
|
||||
case Add:
|
||||
if target.flags[change.mode] {
|
||||
if target.modes.Has(change.mode) {
|
||||
continue
|
||||
}
|
||||
target.flags[change.mode] = true
|
||||
target.modes.Set(change.mode)
|
||||
changes = append(changes, change)
|
||||
|
||||
case Remove:
|
||||
if !target.flags[change.mode] {
|
||||
if !target.modes.Has(change.mode) {
|
||||
continue
|
||||
}
|
||||
delete(target.flags, change.mode)
|
||||
target.modes.Unset(change.mode)
|
||||
changes = append(changes, change)
|
||||
}
|
||||
|
||||
case Operator, LocalOperator:
|
||||
case Operator:
|
||||
if change.op == Remove {
|
||||
if !target.flags[change.mode] {
|
||||
if !target.modes.Has(change.mode) {
|
||||
continue
|
||||
}
|
||||
delete(target.flags, change.mode)
|
||||
target.modes.Unset(change.mode)
|
||||
changes = append(changes, change)
|
||||
}
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@ type OperNickCommand struct {
|
||||
func (msg *OperNickCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
|
||||
if !client.flags[Operator] {
|
||||
if !client.modes.Has(Operator) {
|
||||
client.ErrNoPrivileges()
|
||||
return
|
||||
}
|
||||
|
121
irc/password.go
121
irc/password.go
@@ -1,31 +1,124 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyPasswordError = errors.New("empty password")
|
||||
)
|
||||
var DefaultPasswordHasher = &Base64BCryptPasswordHasher{}
|
||||
|
||||
func GenerateEncodedPassword(passwd string) (encoded string, err error) {
|
||||
if passwd == "" {
|
||||
err = EmptyPasswordError
|
||||
type PasswordHasher interface {
|
||||
Decode(encoded []byte) (decoded []byte, err error)
|
||||
Encode(password []byte) (encoded []byte, err error)
|
||||
Compare(encoded []byte, password []byte) error
|
||||
}
|
||||
|
||||
type PasswordStore interface {
|
||||
Get(username string) ([]byte, bool)
|
||||
Set(username, password string) error
|
||||
Verify(username, password string) error
|
||||
}
|
||||
|
||||
type PasswordStoreOpts struct {
|
||||
hasher PasswordHasher
|
||||
}
|
||||
|
||||
type MemoryPasswordStore struct {
|
||||
sync.RWMutex
|
||||
passwords map[string][]byte
|
||||
hasher PasswordHasher
|
||||
}
|
||||
|
||||
func NewMemoryPasswordStore(passwords map[string][]byte, opts PasswordStoreOpts) *MemoryPasswordStore {
|
||||
var hasher PasswordHasher
|
||||
|
||||
if opts.hasher != nil {
|
||||
hasher = opts.hasher
|
||||
} else {
|
||||
hasher = DefaultPasswordHasher
|
||||
}
|
||||
|
||||
return &MemoryPasswordStore{
|
||||
passwords: passwords,
|
||||
hasher: hasher,
|
||||
}
|
||||
}
|
||||
|
||||
func (store *MemoryPasswordStore) Get(username string) ([]byte, bool) {
|
||||
store.RLock()
|
||||
defer store.RUnlock()
|
||||
|
||||
hash, ok := store.passwords[username]
|
||||
return hash, ok
|
||||
}
|
||||
|
||||
func (store *MemoryPasswordStore) Set(username, password string) error {
|
||||
// Not Implemented
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *MemoryPasswordStore) Verify(username, password string) error {
|
||||
log.Debugf("looking up: %s", username)
|
||||
log.Debugf("%v", store.passwords)
|
||||
hash, ok := store.Get(username)
|
||||
if !ok {
|
||||
log.Debugf("username %s not found", username)
|
||||
return fmt.Errorf("account not found: %s", username)
|
||||
}
|
||||
|
||||
return store.hasher.Compare(hash, []byte(password))
|
||||
}
|
||||
|
||||
type Base64BCryptPasswordHasher struct{}
|
||||
|
||||
func (hasher *Base64BCryptPasswordHasher) Decode(encoded []byte) (decoded []byte, err error) {
|
||||
if encoded == nil {
|
||||
err = fmt.Errorf("empty password")
|
||||
return
|
||||
}
|
||||
bcrypted, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encoded = base64.StdEncoding.EncodeToString(bcrypted)
|
||||
decoded = make([]byte, base64.StdEncoding.DecodedLen(len(encoded)))
|
||||
log.Debugf("Decode:")
|
||||
log.Debugf("decoded: %v", decoded)
|
||||
log.Debugf("encoded: %v", encoded)
|
||||
_, err = base64.StdEncoding.Decode(decoded, encoded)
|
||||
return
|
||||
}
|
||||
|
||||
func (hasher *Base64BCryptPasswordHasher) Encode(password []byte) (encoded []byte, err error) {
|
||||
if password == nil {
|
||||
err = fmt.Errorf("empty password")
|
||||
return
|
||||
}
|
||||
bcrypted, err := bcrypt.GenerateFromPassword(password, bcrypt.MinCost)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base64.StdEncoding.Encode(encoded, bcrypted)
|
||||
return
|
||||
}
|
||||
|
||||
func (hasher *Base64BCryptPasswordHasher) Compare(encoded, password []byte) error {
|
||||
log.Debugf("encoded: %s", encoded)
|
||||
log.Debugf("password: %s", password)
|
||||
decoded, err := hasher.Decode(encoded)
|
||||
log.Debugf("decoded: %s", decoded)
|
||||
log.Debugf("err: %s", err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bcrypt.CompareHashAndPassword(decoded, []byte(password))
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
|
||||
func DecodePassword(encoded string) (decoded []byte, err error) {
|
||||
if encoded == "" {
|
||||
err = EmptyPasswordError
|
||||
err = fmt.Errorf("empty password")
|
||||
return
|
||||
}
|
||||
decoded, err = base64.StdEncoding.DecodeString(encoded)
|
||||
|
22
irc/privacy.go
Normal file
22
irc/privacy.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package irc
|
||||
|
||||
func CanSeeChannel(client *Client, channel *Channel) bool {
|
||||
isPrivate := channel.flags.Has(Private)
|
||||
isSecret := channel.flags.Has(Secret)
|
||||
|
||||
isMember := channel.members.Has(client)
|
||||
isOperator := client.modes.Has(Operator)
|
||||
isRegistered := client.modes.Has(Registered)
|
||||
isSecure := client.modes.Has(SecureConn)
|
||||
|
||||
if !(isSecret || isPrivate) {
|
||||
return true
|
||||
}
|
||||
if isSecret && (isMember || isOperator) {
|
||||
return true
|
||||
}
|
||||
if isPrivate && (isMember || isOperator || (isRegistered && isSecure)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
221
irc/reply.go
221
irc/reply.go
@@ -173,14 +173,18 @@ func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string {
|
||||
// numeric replies
|
||||
|
||||
func (target *Client) RplWelcome() {
|
||||
target.NumericReply(RPL_WELCOME,
|
||||
":Welcome to the Internet Relay Network %s", target.Id())
|
||||
target.NumericReply(
|
||||
RPL_WELCOME,
|
||||
":Welcome to the %s Internet Relay Network %s",
|
||||
target.server.Network(),
|
||||
target.Id(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplYourHost() {
|
||||
target.NumericReply(
|
||||
RPL_YOURHOST,
|
||||
":Your host is %s, running eris version %s",
|
||||
":Your host is %s, running %s",
|
||||
target.server.name,
|
||||
FullVersion(),
|
||||
)
|
||||
@@ -245,23 +249,37 @@ func (target *Client) RplRehashing() {
|
||||
|
||||
func (target *Client) RplWhois(client *Client) {
|
||||
target.RplWhoisUser(client)
|
||||
if client.flags[Operator] {
|
||||
if client.modes.Has(Operator) {
|
||||
target.RplWhoisOperator(client)
|
||||
}
|
||||
target.RplWhoisIdle(client)
|
||||
target.RplWhoisChannels(client)
|
||||
|
||||
if client.flags[SecureConn] {
|
||||
if client.modes.Has(SecureConn) {
|
||||
target.RplWhoisSecure(client)
|
||||
}
|
||||
target.RplWhoisServer(client)
|
||||
target.RplEndOfWhois()
|
||||
target.RplWhoisLoggedIn(client)
|
||||
target.RplEndOfWhois(client)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisUser(client *Client) {
|
||||
target.NumericReply(RPL_WHOISUSER,
|
||||
"%s %s %s * :%s", client.Nick(), client.username, client.hostname,
|
||||
client.realname)
|
||||
var clientHost Name
|
||||
|
||||
if target.modes.Has(Operator) || !client.modes.Has(HostMask) {
|
||||
clientHost = client.hostname
|
||||
} else {
|
||||
clientHost = client.hostmask
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_WHOISUSER,
|
||||
"%s %s %s * :%s",
|
||||
client.Nick(),
|
||||
client.username,
|
||||
clientHost,
|
||||
client.realname,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisOperator(client *Client) {
|
||||
@@ -283,6 +301,19 @@ func (target *Client) RplWhoisIdle(client *Client) {
|
||||
client.Nick(), client.IdleSeconds(), client.SignonTime())
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisLoggedIn(client *Client) {
|
||||
if client.sasl.Id() == "" {
|
||||
return
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_WHOISLOGGEDIN,
|
||||
"%s %s :Is logged in as",
|
||||
client.Nick(),
|
||||
client.sasl.Id(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisServer(client *Client) {
|
||||
target.NumericReply(
|
||||
RPL_WHOISSERVER,
|
||||
@@ -293,9 +324,12 @@ func (target *Client) RplWhoisServer(client *Client) {
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplEndOfWhois() {
|
||||
target.NumericReply(RPL_ENDOFWHOIS,
|
||||
":End of WHOIS list")
|
||||
func (target *Client) RplEndOfWhois(client *Client) {
|
||||
target.NumericReply(
|
||||
RPL_ENDOFWHOIS,
|
||||
"%s :End of WHOIS list",
|
||||
client.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplChannelModeIs(channel *Channel) {
|
||||
@@ -306,38 +340,55 @@ func (target *Client) RplChannelModeIs(channel *Channel) {
|
||||
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
|
||||
// :<hopcount> <real name>
|
||||
func (target *Client) RplWhoReply(channel *Channel, client *Client) {
|
||||
var clientHost Name
|
||||
|
||||
if target.modes.Has(Operator) || !client.modes.Has(HostMask) {
|
||||
clientHost = client.hostname
|
||||
} else {
|
||||
clientHost = client.hostmask
|
||||
}
|
||||
|
||||
channelName := "*"
|
||||
flags := ""
|
||||
|
||||
if client.flags[Away] {
|
||||
if client.modes.Has(Away) {
|
||||
flags = "G"
|
||||
} else {
|
||||
flags = "H"
|
||||
}
|
||||
if client.flags[Operator] {
|
||||
if client.modes.Has(Operator) {
|
||||
flags += "*"
|
||||
}
|
||||
|
||||
if channel != nil {
|
||||
channelName = channel.name.String()
|
||||
if target.capabilities[MultiPrefix] {
|
||||
if channel.members[client][ChannelOperator] {
|
||||
if channel.members.Get(client).Has(ChannelOperator) {
|
||||
flags += "@"
|
||||
}
|
||||
if channel.members[client][Voice] {
|
||||
if channel.members.Get(client).Has(Voice) {
|
||||
flags += "+"
|
||||
}
|
||||
} else {
|
||||
if channel.members[client][ChannelOperator] {
|
||||
if channel.members.Get(client).Has(ChannelOperator) {
|
||||
flags += "@"
|
||||
} else if channel.members[client][Voice] {
|
||||
} else if channel.members.Get(client).Has(Voice) {
|
||||
flags += "+"
|
||||
}
|
||||
}
|
||||
}
|
||||
target.NumericReply(RPL_WHOREPLY,
|
||||
"%s %s %s %s %s %s :%d %s", channelName, client.username, client.hostname,
|
||||
client.server.name, client.Nick(), flags, client.hops, client.realname)
|
||||
target.NumericReply(
|
||||
RPL_WHOREPLY,
|
||||
"%s %s %s %s %s %s :%d %s",
|
||||
channelName,
|
||||
client.username,
|
||||
clientHost,
|
||||
client.server.name,
|
||||
client.Nick(),
|
||||
flags,
|
||||
client.hops,
|
||||
client.realname,
|
||||
)
|
||||
}
|
||||
|
||||
// <name> :End of WHO list
|
||||
@@ -438,8 +489,13 @@ func (target *Client) RplMOTDEnd() {
|
||||
}
|
||||
|
||||
func (target *Client) RplList(channel *Channel) {
|
||||
target.NumericReply(RPL_LIST,
|
||||
"%s %d :%s", channel, len(channel.members), channel.topic)
|
||||
target.NumericReply(
|
||||
RPL_LIST,
|
||||
"%s %d :%s",
|
||||
channel,
|
||||
channel.members.Count(),
|
||||
channel.topic,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplListEnd(server *Server) {
|
||||
@@ -453,8 +509,12 @@ func (target *Client) RplNamReply(channel *Channel) {
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoisChannels(client *Client) {
|
||||
target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS,
|
||||
"%s :%s", client.Nick())
|
||||
target.MultilineReply(
|
||||
client.WhoisChannelsNames(target),
|
||||
RPL_WHOISCHANNELS,
|
||||
"%s :%s",
|
||||
client.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplVersion() {
|
||||
@@ -490,7 +550,7 @@ func (target *Client) RplLUserClient() {
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserUnknown() {
|
||||
nUnknown := target.server.connections - len(target.server.clients.byNick)
|
||||
nUnknown := target.server.connections.Value() - target.server.clients.Count()
|
||||
|
||||
if nUnknown == 0 {
|
||||
return
|
||||
@@ -504,7 +564,7 @@ func (target *Client) RplLUserUnknown() {
|
||||
}
|
||||
|
||||
func (target *Client) RplLUserChannels() {
|
||||
nChannels := target.server.channels.Length()
|
||||
nChannels := target.server.channels.Count()
|
||||
if nChannels == 0 {
|
||||
return
|
||||
}
|
||||
@@ -518,11 +578,12 @@ func (target *Client) RplLUserChannels() {
|
||||
|
||||
func (target *Client) RplLUserOp() {
|
||||
nOperators := 0
|
||||
for _, client := range target.server.clients.byNick {
|
||||
if client.flags[Operator] {
|
||||
nOperators += 1
|
||||
target.server.clients.Range(func(_ Name, client *Client) bool {
|
||||
if client.modes.Has(Operator) {
|
||||
nOperators++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if nOperators == 0 {
|
||||
return
|
||||
@@ -547,9 +608,22 @@ func (target *Client) RplLUserMe() {
|
||||
}
|
||||
|
||||
func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
|
||||
target.NumericReply(RPL_WHOWASUSER,
|
||||
var whoWasHost Name
|
||||
|
||||
if target.modes.Has(Operator) {
|
||||
whoWasHost = whoWas.hostname
|
||||
} else {
|
||||
whoWasHost = whoWas.hostmask
|
||||
}
|
||||
|
||||
target.NumericReply(
|
||||
RPL_WHOWASUSER,
|
||||
"%s %s %s * :%s",
|
||||
whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname)
|
||||
whoWas.nickname,
|
||||
whoWas.username,
|
||||
whoWasHost,
|
||||
whoWas.realname,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplEndOfWhoWas(nickname Name) {
|
||||
@@ -709,3 +783,84 @@ func (target *Client) ErrInviteOnlyChan(channel *Channel) {
|
||||
target.NumericReply(ERR_INVITEONLYCHAN,
|
||||
"%s :Cannot join channel (+i)", channel)
|
||||
}
|
||||
|
||||
//
|
||||
// SASL Errors / Replies
|
||||
//
|
||||
|
||||
func RplAuthenticate(client *Client, arg string) string {
|
||||
return NewStringReply(client.server, AUTHENTICATE, arg)
|
||||
}
|
||||
|
||||
func (target *Client) RplLoggedIn(authcid string) {
|
||||
target.NumericReply(
|
||||
RPL_LOGGEDIN,
|
||||
"%s %s :You are now logged in as %s",
|
||||
target, authcid, authcid,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplLoggedOut() {
|
||||
target.NumericReply(
|
||||
RPL_LOGGEDIN,
|
||||
"%s :You are now logged out",
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrNickLocked() {
|
||||
target.NumericReply(
|
||||
ERR_NICKLOCKED,
|
||||
"%s :You must use a nick assigned to you",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplSaslSuccess() {
|
||||
target.NumericReply(
|
||||
RPL_SASLSUCCESS,
|
||||
"%s :SASL authentication successful",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslFail(message string) {
|
||||
target.NumericReply(
|
||||
ERR_SASLFAIL,
|
||||
"%s :SASL authentication failed: %s",
|
||||
target.Nick(), message,
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslTooLong() {
|
||||
target.NumericReply(
|
||||
ERR_SASLFAIL,
|
||||
"%s :SASL message too long",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslAborted() {
|
||||
target.NumericReply(
|
||||
ERR_SASLABORTED,
|
||||
"%s :SASL authentication aborted",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) ErrSaslAlready() {
|
||||
target.NumericReply(
|
||||
ERR_SASLALREADY,
|
||||
"%s :You have already authenticated using SASL",
|
||||
target.Nick(),
|
||||
)
|
||||
}
|
||||
|
||||
func (target *Client) RplSaslMechs(mechs ...string) {
|
||||
target.NumericReply(
|
||||
RPL_SASLMECHS,
|
||||
"%s %s :are available SASL mechanisms",
|
||||
target.Nick(),
|
||||
strings.Join(mechs, ","),
|
||||
)
|
||||
}
|
||||
|
84
irc/sasl.go
Normal file
84
irc/sasl.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type SaslState struct {
|
||||
sync.RWMutex
|
||||
|
||||
started bool
|
||||
|
||||
buffer *bytes.Buffer
|
||||
mech string
|
||||
|
||||
authcid string
|
||||
}
|
||||
|
||||
func NewSaslState() *SaslState {
|
||||
return &SaslState{buffer: &bytes.Buffer{}}
|
||||
}
|
||||
|
||||
func (s *SaslState) Reset() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.started = false
|
||||
s.buffer.Reset()
|
||||
s.mech = ""
|
||||
s.authcid = ""
|
||||
}
|
||||
|
||||
func (s *SaslState) Started() bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.started
|
||||
}
|
||||
|
||||
func (s *SaslState) Start() {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.started = true
|
||||
}
|
||||
|
||||
func (s *SaslState) WriteString(data string) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.buffer.WriteString(data)
|
||||
}
|
||||
|
||||
func (s SaslState) Len() int {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.buffer.Len()
|
||||
}
|
||||
|
||||
func (s *SaslState) String() string {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.buffer.String()
|
||||
}
|
||||
|
||||
func (s *SaslState) Login(authcid string) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.started = false
|
||||
s.buffer.Reset()
|
||||
s.mech = ""
|
||||
|
||||
s.authcid = authcid
|
||||
}
|
||||
|
||||
func (s *SaslState) Id() string {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.authcid
|
||||
}
|
346
irc/server.go
346
irc/server.go
@@ -2,8 +2,10 @@ package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -27,42 +29,60 @@ type RegServerCommand interface {
|
||||
|
||||
type Server struct {
|
||||
config *Config
|
||||
metrics *Metrics
|
||||
channels *ChannelNameMap
|
||||
connections int
|
||||
connections *Counter
|
||||
clients *ClientLookupSet
|
||||
ctime time.Time
|
||||
idle chan *Client
|
||||
motdFile string
|
||||
name Name
|
||||
network Name
|
||||
description string
|
||||
newConns chan net.Conn
|
||||
operators map[Name][]byte
|
||||
accounts PasswordStore
|
||||
password []byte
|
||||
signals chan os.Signal
|
||||
done chan bool
|
||||
whoWas *WhoWasList
|
||||
ids map[string]*Identity
|
||||
}
|
||||
|
||||
var (
|
||||
SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
|
||||
syscall.SIGTERM, syscall.SIGQUIT}
|
||||
SERVER_SIGNALS = []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
)
|
||||
|
||||
func NewServer(config *Config) *Server {
|
||||
server := &Server{
|
||||
config: config,
|
||||
metrics: NewMetrics("eris"),
|
||||
channels: NewChannelNameMap(),
|
||||
connections: &Counter{},
|
||||
clients: NewClientLookupSet(),
|
||||
ctime: time.Now(),
|
||||
idle: make(chan *Client),
|
||||
motdFile: config.Server.MOTD,
|
||||
name: NewName(config.Server.Name),
|
||||
network: NewName(config.Network.Name),
|
||||
description: config.Server.Description,
|
||||
newConns: make(chan net.Conn),
|
||||
operators: config.Operators(),
|
||||
accounts: NewMemoryPasswordStore(config.Accounts(), PasswordStoreOpts{}),
|
||||
signals: make(chan os.Signal, len(SERVER_SIGNALS)),
|
||||
done: make(chan bool),
|
||||
whoWas: NewWhoWasList(100),
|
||||
ids: make(map[string]*Identity),
|
||||
}
|
||||
|
||||
log.Debugf("accounts: %v", config.Accounts())
|
||||
|
||||
// TODO: Make this configureable?
|
||||
server.ids["global"] = NewIdentity(config.Server.Name, "global")
|
||||
|
||||
if config.Server.Password != "" {
|
||||
server.password = config.Server.PasswordBytes()
|
||||
}
|
||||
@@ -77,34 +97,127 @@ func NewServer(config *Config) *Server {
|
||||
|
||||
signal.Notify(server.signals, SERVER_SIGNALS...)
|
||||
|
||||
// server uptime counter
|
||||
server.metrics.NewCounterFunc(
|
||||
"server", "uptime",
|
||||
"Number of seconds the server has been running",
|
||||
func() float64 {
|
||||
return float64(time.Since(server.ctime).Nanoseconds())
|
||||
},
|
||||
)
|
||||
|
||||
// client commands counter
|
||||
server.metrics.NewCounter(
|
||||
"client", "commands",
|
||||
"Number of client commands processed",
|
||||
)
|
||||
|
||||
// client messages counter
|
||||
server.metrics.NewCounter(
|
||||
"client", "messages",
|
||||
"Number of client messages exchanged",
|
||||
)
|
||||
|
||||
// server connections gauge
|
||||
server.metrics.NewGaugeFunc(
|
||||
"server", "connections",
|
||||
"Number of active connections to the server",
|
||||
func() float64 {
|
||||
return float64(server.connections.Value())
|
||||
},
|
||||
)
|
||||
|
||||
// server registered (clients) gauge
|
||||
server.metrics.NewGaugeFunc(
|
||||
"server", "registered",
|
||||
"Number of registered clients connected",
|
||||
func() float64 {
|
||||
return float64(server.clients.Count())
|
||||
},
|
||||
)
|
||||
|
||||
// server clients gauge (by secure/insecure)
|
||||
server.metrics.NewGaugeVec(
|
||||
"server", "clients",
|
||||
"Number of registered clients connected (by secure/insecure)",
|
||||
[]string{"secure"},
|
||||
)
|
||||
|
||||
// server channels gauge
|
||||
server.metrics.NewGaugeFunc(
|
||||
"server", "channels",
|
||||
"Number of active channels",
|
||||
func() float64 {
|
||||
return float64(server.channels.Count())
|
||||
},
|
||||
)
|
||||
|
||||
// client command processing time summaries
|
||||
server.metrics.NewSummaryVec(
|
||||
"client", "command_duration_seconds",
|
||||
"Client command processing time in seconds",
|
||||
[]string{"command"},
|
||||
)
|
||||
|
||||
// client ping latency summary
|
||||
server.metrics.NewSummary(
|
||||
"client", "ping_latency_seconds",
|
||||
"Client ping latency in seconds",
|
||||
)
|
||||
|
||||
go server.metrics.Run(":9314")
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (server *Server) Wallops(message string) {
|
||||
for _, client := range server.clients.byNick {
|
||||
if client.flags[WallOps] {
|
||||
client.Reply(RplNotice(server, client, NewText(message)))
|
||||
text := NewText(message)
|
||||
server.clients.Range(func(_ Name, client *Client) bool {
|
||||
if client.modes.Has(WallOps) {
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
client.replies <- RplNotice(server, client, text)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (server *Server) Wallopsf(format string, args ...interface{}) {
|
||||
server.Wallops(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (server *Server) Global(message string) {
|
||||
text := NewText(message)
|
||||
server.clients.Range(func(_ Name, client *Client) bool {
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
client.replies <- RplNotice(server.ids["global"], client, text)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (server *Server) Globalf(format string, args ...interface{}) {
|
||||
server.Global(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (server *Server) Shutdown() {
|
||||
for _, client := range server.clients.byNick {
|
||||
client.Reply(RplNotice(server, client, "shutting down"))
|
||||
}
|
||||
server.Global("shutting down...")
|
||||
}
|
||||
|
||||
func (server *Server) Stop() {
|
||||
server.done <- true
|
||||
}
|
||||
|
||||
func (server *Server) Run() {
|
||||
done := false
|
||||
for !done {
|
||||
for {
|
||||
select {
|
||||
case <-server.done:
|
||||
return
|
||||
case <-server.signals:
|
||||
server.Shutdown()
|
||||
done = true
|
||||
// Give at least 1s for clients to see the shutdown
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
server.Stop()
|
||||
}()
|
||||
|
||||
case conn := <-server.newConns:
|
||||
go NewClient(server, conn)
|
||||
@@ -124,7 +237,13 @@ func (s *Server) acceptor(listener net.Listener) {
|
||||
}
|
||||
log.Debugf("%s accept: %s", s, conn.RemoteAddr())
|
||||
|
||||
s.connections += 1
|
||||
if _, ok := conn.(*tls.Conn); ok {
|
||||
s.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Inc()
|
||||
} else {
|
||||
s.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Inc()
|
||||
}
|
||||
|
||||
s.connections.Inc()
|
||||
s.newConns <- conn
|
||||
}
|
||||
}
|
||||
@@ -223,6 +342,7 @@ func (s *Server) Rehash() error {
|
||||
|
||||
s.motdFile = s.config.Server.MOTD
|
||||
s.name = NewName(s.config.Server.Name)
|
||||
s.network = NewName(s.config.Network.Name)
|
||||
s.description = s.config.Server.Description
|
||||
s.operators = s.config.Operators()
|
||||
|
||||
@@ -233,6 +353,10 @@ func (s *Server) Id() Name {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s *Server) Network() Name {
|
||||
return s.network
|
||||
}
|
||||
|
||||
func (s *Server) String() string {
|
||||
return s.name.String()
|
||||
}
|
||||
@@ -284,13 +408,104 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
|
||||
flags := msg.Flags()
|
||||
if len(flags) > 0 {
|
||||
for _, mode := range flags {
|
||||
client.flags[mode] = true
|
||||
client.modes.Set(mode)
|
||||
}
|
||||
client.RplUModeIs(client)
|
||||
}
|
||||
msg.setUserInfo(server)
|
||||
}
|
||||
|
||||
func (msg *AuthenticateCommand) HandleRegServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if !client.authorized {
|
||||
client.ErrPasswdMismatch()
|
||||
client.Quit("bad password")
|
||||
return
|
||||
}
|
||||
|
||||
if msg.arg == "*" {
|
||||
client.ErrSaslAborted()
|
||||
return
|
||||
}
|
||||
|
||||
if !client.sasl.Started() {
|
||||
if msg.arg == "PLAIN" {
|
||||
client.sasl.Start()
|
||||
client.Reply(RplAuthenticate(client, "+"))
|
||||
} else {
|
||||
client.RplSaslMechs("PLAIN")
|
||||
client.ErrSaslFail("Unknown authentication mechanism")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(msg.arg) > 400 {
|
||||
client.ErrSaslTooLong()
|
||||
return
|
||||
}
|
||||
|
||||
if len(msg.arg) == 400 {
|
||||
client.sasl.WriteString(msg.arg)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.arg != "+" {
|
||||
client.sasl.WriteString(msg.arg)
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(client.sasl.String())
|
||||
if err != nil {
|
||||
client.ErrSaslFail("Invalid base64 encoding")
|
||||
client.sasl.Reset()
|
||||
return
|
||||
}
|
||||
|
||||
// Do authentication
|
||||
|
||||
var (
|
||||
authcid string
|
||||
authzid string
|
||||
password string
|
||||
)
|
||||
|
||||
tokens := bytes.Split(data, []byte{'\000'})
|
||||
if len(tokens) == 3 {
|
||||
authcid = string(tokens[0])
|
||||
authzid = string(tokens[1])
|
||||
password = string(tokens[2])
|
||||
|
||||
if authzid == "" {
|
||||
authzid = authcid
|
||||
} else if authzid != authcid {
|
||||
client.ErrSaslFail("authzid and authcid should be the same")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
client.ErrSaslFail("invalid authentication blob")
|
||||
return
|
||||
}
|
||||
|
||||
err = server.accounts.Verify(authcid, password)
|
||||
if err != nil {
|
||||
client.ErrSaslFail("invalid authentication")
|
||||
return
|
||||
}
|
||||
|
||||
client.sasl.Login(authcid)
|
||||
client.RplLoggedIn(authcid)
|
||||
client.RplSaslSuccess()
|
||||
|
||||
client.modes.Set(Registered)
|
||||
client.Reply(
|
||||
RplModeChanges(
|
||||
client, client,
|
||||
ModeChanges{
|
||||
&ModeChange{mode: Registered, op: Add},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (msg *UserCommand) setUserInfo(server *Server) {
|
||||
client := msg.Client()
|
||||
|
||||
@@ -319,7 +534,8 @@ func (m *PingCommand) HandleServer(s *Server) {
|
||||
}
|
||||
|
||||
func (m *PongCommand) HandleServer(s *Server) {
|
||||
// no-op
|
||||
v := s.metrics.Summary("client", "ping_latency_seconds")
|
||||
v.Observe(time.Now().Sub(m.Client().pingTime).Seconds())
|
||||
}
|
||||
|
||||
func (m *UserCommand) HandleServer(s *Server) {
|
||||
@@ -334,9 +550,10 @@ func (m *JoinCommand) HandleServer(s *Server) {
|
||||
client := m.Client()
|
||||
|
||||
if m.zero {
|
||||
for channel := range client.channels {
|
||||
client.channels.Range(func(channel *Channel) bool {
|
||||
channel.Part(client, client.Nick().Text())
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -405,28 +622,34 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) {
|
||||
client.ErrCannotSendToUser(target.nick, "secure connection required")
|
||||
return
|
||||
}
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
target.Reply(RplPrivMsg(client, target, msg.message))
|
||||
if target.flags[Away] {
|
||||
if target.modes.Has(Away) {
|
||||
client.RplAway(target)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) WhoisChannelsNames() []string {
|
||||
chstrs := make([]string, len(client.channels))
|
||||
func (client *Client) WhoisChannelsNames(target *Client) []string {
|
||||
chstrs := make([]string, client.channels.Count())
|
||||
index := 0
|
||||
for channel := range client.channels {
|
||||
client.channels.Range(func(channel *Channel) bool {
|
||||
if !CanSeeChannel(target, channel) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch {
|
||||
case channel.members[client][ChannelOperator]:
|
||||
case channel.members.Get(client).Has(ChannelOperator):
|
||||
chstrs[index] = "@" + channel.name.String()
|
||||
|
||||
case channel.members[client][Voice]:
|
||||
case channel.members.Get(client).Has(Voice):
|
||||
chstrs[index] = "+" + channel.name.String()
|
||||
|
||||
default:
|
||||
chstrs[index] = channel.name.String()
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
index++
|
||||
return true
|
||||
})
|
||||
return chstrs
|
||||
}
|
||||
|
||||
@@ -437,22 +660,24 @@ func (m *WhoisCommand) HandleServer(server *Server) {
|
||||
|
||||
for _, mask := range m.masks {
|
||||
matches := server.clients.FindAll(mask)
|
||||
if len(matches) == 0 {
|
||||
if matches.Count() == 0 {
|
||||
client.ErrNoSuchNick(mask)
|
||||
continue
|
||||
}
|
||||
for mclient := range matches {
|
||||
matches.Range(func(mclient *Client) bool {
|
||||
client.RplWhois(mclient)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func whoChannel(client *Client, channel *Channel, friends ClientSet) {
|
||||
for member := range channel.members {
|
||||
if !client.flags[Invisible] || friends[client] {
|
||||
func whoChannel(client *Client, channel *Channel, friends *ClientSet) {
|
||||
channel.members.Range(func(member *Client, _ *ChannelModeSet) bool {
|
||||
if !client.modes.Has(Invisible) || friends.Has(client) {
|
||||
client.RplWhoReply(channel, member)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (msg *WhoCommand) HandleServer(server *Server) {
|
||||
@@ -472,9 +697,11 @@ func (msg *WhoCommand) HandleServer(server *Server) {
|
||||
whoChannel(client, channel, friends)
|
||||
}
|
||||
} else {
|
||||
for mclient := range server.clients.FindAll(mask) {
|
||||
matches := server.clients.FindAll(mask)
|
||||
matches.Range(func(mclient *Client) bool {
|
||||
client.RplWhoReply(nil, mclient)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
client.RplEndOfWho(mask)
|
||||
@@ -488,17 +715,23 @@ func (msg *OperCommand) HandleServer(server *Server) {
|
||||
return
|
||||
}
|
||||
|
||||
client.flags[Operator] = true
|
||||
client.modes.Set(Operator)
|
||||
client.modes.Set(WallOps)
|
||||
client.RplYoureOper()
|
||||
client.Reply(RplModeChanges(client, client, ModeChanges{&ModeChange{
|
||||
mode: Operator,
|
||||
op: Add,
|
||||
}}))
|
||||
client.Reply(
|
||||
RplModeChanges(
|
||||
client, client,
|
||||
ModeChanges{
|
||||
&ModeChange{mode: Operator, op: Add},
|
||||
&ModeChange{mode: WallOps, op: Add},
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (msg *RehashCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if !client.flags[Operator] {
|
||||
if !client.modes.Has(Operator) {
|
||||
client.ErrNoPrivileges()
|
||||
return
|
||||
}
|
||||
@@ -523,9 +756,9 @@ func (msg *RehashCommand) HandleServer(server *Server) {
|
||||
func (msg *AwayCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if len(msg.text) > 0 {
|
||||
client.flags[Away] = true
|
||||
client.modes.Set(Away)
|
||||
} else {
|
||||
delete(client.flags, Away)
|
||||
client.modes.Unset(Away)
|
||||
}
|
||||
client.awayMessage = msg.text
|
||||
}
|
||||
@@ -549,6 +782,12 @@ func (msg *MOTDCommand) HandleServer(server *Server) {
|
||||
|
||||
func (msg *NoticeCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
|
||||
if msg.target == "*" && client.modes.Has(Operator) {
|
||||
server.Global(msg.message.String())
|
||||
return
|
||||
}
|
||||
|
||||
if msg.target.IsChannel() {
|
||||
channel := server.channels.Get(msg.target)
|
||||
if channel == nil {
|
||||
@@ -570,6 +809,7 @@ func (msg *NoticeCommand) HandleServer(server *Server) {
|
||||
client.ErrCannotSendToUser(target.nick, "secure connection required")
|
||||
return
|
||||
}
|
||||
server.metrics.Counter("client", "messages").Inc()
|
||||
target.Reply(RplNotice(client, target, msg.message))
|
||||
}
|
||||
|
||||
@@ -603,16 +843,16 @@ func (msg *ListCommand) HandleServer(server *Server) {
|
||||
|
||||
if len(msg.channels) == 0 {
|
||||
server.channels.Range(func(name Name, channel *Channel) bool {
|
||||
if !client.flags[Operator] && channel.flags[Private] {
|
||||
if !CanSeeChannel(client, channel) {
|
||||
return true
|
||||
}
|
||||
client.RplList(channel)
|
||||
return false
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
for _, chname := range msg.channels {
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil || (!client.flags[Operator] && channel.flags[Private]) {
|
||||
if channel == nil || !CanSeeChannel(client, channel) {
|
||||
client.ErrNoSuchChannel(chname)
|
||||
continue
|
||||
}
|
||||
@@ -624,7 +864,7 @@ func (msg *ListCommand) HandleServer(server *Server) {
|
||||
|
||||
func (msg *NamesCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if server.channels.Length() == 0 {
|
||||
if server.channels.Count() == 0 {
|
||||
server.channels.Range(func(name Name, channel *Channel) bool {
|
||||
channel.Names(client)
|
||||
return true
|
||||
@@ -690,9 +930,19 @@ func (msg *LUsersCommand) HandleServer(server *Server) {
|
||||
client.RplLUserMe()
|
||||
}
|
||||
|
||||
func (msg *WallopsCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if !client.modes.Has(Operator) {
|
||||
client.ErrNoPrivileges()
|
||||
return
|
||||
}
|
||||
|
||||
server.Wallops(msg.message.String())
|
||||
}
|
||||
|
||||
func (msg *KillCommand) HandleServer(server *Server) {
|
||||
client := msg.Client()
|
||||
if !client.flags[Operator] {
|
||||
if !client.modes.Has(Operator) {
|
||||
client.ErrNoPrivileges()
|
||||
return
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -14,10 +15,11 @@ const (
|
||||
)
|
||||
|
||||
type Socket struct {
|
||||
closed bool
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
writer *bufio.Writer
|
||||
closed bool
|
||||
closedMutex sync.RWMutex
|
||||
conn net.Conn
|
||||
scanner *bufio.Scanner
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func NewSocket(conn net.Conn) *Socket {
|
||||
@@ -33,6 +35,9 @@ func (socket *Socket) String() string {
|
||||
}
|
||||
|
||||
func (socket *Socket) Close() {
|
||||
socket.closedMutex.Lock()
|
||||
defer socket.closedMutex.Unlock()
|
||||
|
||||
if socket.closed {
|
||||
return
|
||||
}
|
||||
@@ -42,6 +47,8 @@ func (socket *Socket) Close() {
|
||||
}
|
||||
|
||||
func (socket *Socket) Read() (line string, err error) {
|
||||
socket.closedMutex.RLock()
|
||||
defer socket.closedMutex.RUnlock()
|
||||
if socket.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
@@ -65,6 +72,8 @@ func (socket *Socket) Read() (line string, err error) {
|
||||
}
|
||||
|
||||
func (socket *Socket) Write(line string) (err error) {
|
||||
socket.closedMutex.RLock()
|
||||
defer socket.closedMutex.RUnlock()
|
||||
if socket.closed {
|
||||
err = io.EOF
|
||||
return
|
||||
|
333
irc/types.go
333
irc/types.go
@@ -10,6 +10,29 @@ import (
|
||||
// simple types
|
||||
//
|
||||
|
||||
type Counter struct {
|
||||
sync.RWMutex
|
||||
value int
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.value++
|
||||
}
|
||||
|
||||
func (c *Counter) Dec() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.value--
|
||||
}
|
||||
|
||||
func (c *Counter) Value() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return c.value
|
||||
}
|
||||
|
||||
// ChannelNameMap holds a mapping of channel names to *Channel structs
|
||||
// that is safe for concurrent readers and writers.
|
||||
type ChannelNameMap struct {
|
||||
@@ -24,8 +47,8 @@ func NewChannelNameMap() *ChannelNameMap {
|
||||
}
|
||||
}
|
||||
|
||||
// Length returns the number of *Channel9s)
|
||||
func (c *ChannelNameMap) Length() int {
|
||||
// Count returns the number of *Channel9s)
|
||||
func (c *ChannelNameMap) Count() int {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
return len(c.channels)
|
||||
@@ -37,7 +60,7 @@ func (c *ChannelNameMap) Range(f func(kay Name, value *Channel) bool) {
|
||||
defer c.Unlock()
|
||||
for k, v := range c.channels {
|
||||
if !f(k, v) {
|
||||
break
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,82 +94,300 @@ func (c *ChannelNameMap) Remove(channel *Channel) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ChannelModeSet map[ChannelMode]bool
|
||||
// UserModeSet holds a mapping of channel modes
|
||||
type UserModeSet struct {
|
||||
sync.RWMutex
|
||||
modes map[UserMode]bool
|
||||
}
|
||||
|
||||
func (set ChannelModeSet) String() string {
|
||||
if len(set) == 0 {
|
||||
// NewUserModeSet returns a new UserModeSet
|
||||
func NewUserModeSet() *UserModeSet {
|
||||
return &UserModeSet{modes: make(map[UserMode]bool)}
|
||||
}
|
||||
|
||||
// Set sets mode
|
||||
func (set *UserModeSet) Set(mode UserMode) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
set.modes[mode] = true
|
||||
}
|
||||
|
||||
// Unset unsets mode
|
||||
func (set *UserModeSet) Unset(mode UserMode) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(set.modes, mode)
|
||||
}
|
||||
|
||||
// Has returns true if the mode is set
|
||||
func (set *UserModeSet) Has(mode UserMode) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
ok, _ := set.modes[mode]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Range ranges of the modes calling f
|
||||
func (set *UserModeSet) Range(f func(mode UserMode) bool) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
for mode := range set.modes {
|
||||
if !f(mode) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representing the channel modes
|
||||
func (set *UserModeSet) String() string {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
|
||||
if len(set.modes) == 0 {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, len(set))
|
||||
strs := make([]string, len(set.modes))
|
||||
index := 0
|
||||
for mode := range set {
|
||||
for mode := range set.modes {
|
||||
strs[index] = mode.String()
|
||||
index += 1
|
||||
index++
|
||||
}
|
||||
return strings.Join(strs, "")
|
||||
}
|
||||
|
||||
type ClientSet map[*Client]bool
|
||||
|
||||
func (clients ClientSet) Add(client *Client) {
|
||||
clients[client] = true
|
||||
// ChannelModeSet holds a mapping of channel modes
|
||||
type ChannelModeSet struct {
|
||||
sync.RWMutex
|
||||
modes map[ChannelMode]bool
|
||||
}
|
||||
|
||||
func (clients ClientSet) Remove(client *Client) {
|
||||
delete(clients, client)
|
||||
// NewChannelModeSet returns a new ChannelModeSet
|
||||
func NewChannelModeSet() *ChannelModeSet {
|
||||
return &ChannelModeSet{modes: make(map[ChannelMode]bool)}
|
||||
}
|
||||
|
||||
func (clients ClientSet) Has(client *Client) bool {
|
||||
return clients[client]
|
||||
// Set sets mode
|
||||
func (set *ChannelModeSet) Set(mode ChannelMode) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
set.modes[mode] = true
|
||||
}
|
||||
|
||||
type MemberSet map[*Client]ChannelModeSet
|
||||
|
||||
func (members MemberSet) Add(member *Client) {
|
||||
members[member] = make(ChannelModeSet)
|
||||
// Unset unsets mode
|
||||
func (set *ChannelModeSet) Unset(mode ChannelMode) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(set.modes, mode)
|
||||
}
|
||||
|
||||
func (members MemberSet) Remove(member *Client) {
|
||||
delete(members, member)
|
||||
}
|
||||
|
||||
func (members MemberSet) Has(member *Client) bool {
|
||||
_, ok := members[member]
|
||||
// Has returns true if the mode is set
|
||||
func (set *ChannelModeSet) Has(mode ChannelMode) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
ok, _ := set.modes[mode]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (members MemberSet) HasMode(member *Client, mode ChannelMode) bool {
|
||||
modes, ok := members[member]
|
||||
// Range ranges of the modes calling f
|
||||
func (set *ChannelModeSet) Range(f func(mode ChannelMode) bool) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
for mode := range set.modes {
|
||||
if !f(mode) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a string representing the channel modes
|
||||
func (set *ChannelModeSet) String() string {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
|
||||
if len(set.modes) == 0 {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, len(set.modes))
|
||||
index := 0
|
||||
for mode := range set.modes {
|
||||
strs[index] = mode.String()
|
||||
index++
|
||||
}
|
||||
return strings.Join(strs, "")
|
||||
}
|
||||
|
||||
type ClientSet struct {
|
||||
sync.RWMutex
|
||||
clients map[*Client]bool
|
||||
}
|
||||
|
||||
func NewClientSet() *ClientSet {
|
||||
return &ClientSet{clients: make(map[*Client]bool)}
|
||||
}
|
||||
|
||||
func (set *ClientSet) Add(client *Client) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
set.clients[client] = true
|
||||
}
|
||||
|
||||
func (set *ClientSet) Remove(client *Client) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(set.clients, client)
|
||||
}
|
||||
|
||||
func (set *ClientSet) Count() int {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
return len(set.clients)
|
||||
}
|
||||
|
||||
func (set *ClientSet) Has(client *Client) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
ok, _ := set.clients[client]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (set *ClientSet) Range(f func(client *Client) bool) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
for client := range set.clients {
|
||||
if !f(client) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MemberSet struct {
|
||||
sync.RWMutex
|
||||
members map[*Client]*ChannelModeSet
|
||||
}
|
||||
|
||||
func NewMemberSet() *MemberSet {
|
||||
return &MemberSet{members: make(map[*Client]*ChannelModeSet)}
|
||||
}
|
||||
|
||||
func (set *MemberSet) Count() int {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
return len(set.members)
|
||||
}
|
||||
|
||||
func (set *MemberSet) Range(f func(client *Client, modes *ChannelModeSet) bool) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
for client, modes := range set.members {
|
||||
if !f(client, modes) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (set *MemberSet) Add(member *Client) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
set.members[member] = NewChannelModeSet()
|
||||
}
|
||||
|
||||
func (set *MemberSet) Remove(member *Client) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(set.members, member)
|
||||
}
|
||||
|
||||
func (set *MemberSet) Has(member *Client) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
_, ok := set.members[member]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (set *MemberSet) Get(member *Client) *ChannelModeSet {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
return set.members[member]
|
||||
}
|
||||
|
||||
func (set *MemberSet) HasMode(member *Client, mode ChannelMode) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
modes, ok := set.members[member]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return modes[mode]
|
||||
return modes.Has(mode)
|
||||
}
|
||||
|
||||
func (members MemberSet) AnyHasMode(mode ChannelMode) bool {
|
||||
for _, modes := range members {
|
||||
if modes[mode] {
|
||||
return true
|
||||
type ChannelSet struct {
|
||||
sync.RWMutex
|
||||
channels map[*Channel]bool
|
||||
}
|
||||
|
||||
func NewChannelSet() *ChannelSet {
|
||||
return &ChannelSet{channels: make(map[*Channel]bool)}
|
||||
}
|
||||
|
||||
func (set *ChannelSet) Count() int {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
return len(set.channels)
|
||||
}
|
||||
|
||||
func (set *ChannelSet) Add(channel *Channel) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
set.channels[channel] = true
|
||||
}
|
||||
|
||||
func (set *ChannelSet) Remove(channel *Channel) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
delete(set.channels, channel)
|
||||
}
|
||||
|
||||
func (set *ChannelSet) Range(f func(channel *Channel) bool) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
for channel := range set.channels {
|
||||
if !f(channel) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ChannelSet map[*Channel]bool
|
||||
|
||||
func (channels ChannelSet) Add(channel *Channel) {
|
||||
channels[channel] = true
|
||||
type Identity struct {
|
||||
nickname string
|
||||
username string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func (channels ChannelSet) Remove(channel *Channel) {
|
||||
delete(channels, channel)
|
||||
}
|
||||
func NewIdentity(hostname string, args ...string) *Identity {
|
||||
id := &Identity{hostname: hostname}
|
||||
|
||||
func (channels ChannelSet) First() *Channel {
|
||||
for channel := range channels {
|
||||
return channel
|
||||
if len(args) > 0 {
|
||||
id.nickname = args[0]
|
||||
}
|
||||
return nil
|
||||
if len(args) > 2 {
|
||||
id.username = args[1]
|
||||
} else {
|
||||
id.username = id.nickname
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (id *Identity) Id() Name {
|
||||
return NewName(id.username)
|
||||
}
|
||||
|
||||
func (id *Identity) Nick() Name {
|
||||
return NewName(id.nickname)
|
||||
}
|
||||
|
||||
func (id *Identity) String() string {
|
||||
return fmt.Sprintf("%s!%s@%s", id.nickname, id.username, id.hostname)
|
||||
}
|
||||
|
||||
//
|
||||
|
11
irc/utils.go
Normal file
11
irc/utils.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func SHA256(data string) string {
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
package irc
|
||||
|
||||
var (
|
||||
// Version release version
|
||||
Version = "1.5.4"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Build will be overwritten automatically by the build system
|
||||
Build = "-dev"
|
||||
var (
|
||||
// Package package name
|
||||
Package = "eris"
|
||||
|
||||
// Version release version
|
||||
Version = "1.6.4"
|
||||
|
||||
// GitCommit will be overwritten automatically by the build system
|
||||
GitCommit = "HEAD"
|
||||
@@ -13,5 +17,5 @@ var (
|
||||
|
||||
// FullVersion display the full version and build
|
||||
func FullVersion() string {
|
||||
return Version + Build + " (" + GitCommit + ")"
|
||||
return fmt.Sprintf("%s-%s@%s", Package, Version, GitCommit)
|
||||
}
|
||||
|
@@ -1,6 +1,11 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type WhoWasList struct {
|
||||
sync.RWMutex
|
||||
buffer []*WhoWas
|
||||
start int
|
||||
end int
|
||||
@@ -10,6 +15,7 @@ type WhoWas struct {
|
||||
nickname Name
|
||||
username Name
|
||||
hostname Name
|
||||
hostmask Name
|
||||
realname Text
|
||||
}
|
||||
|
||||
@@ -20,10 +26,13 @@ func NewWhoWasList(size uint) *WhoWasList {
|
||||
}
|
||||
|
||||
func (list *WhoWasList) Append(client *Client) {
|
||||
list.Lock()
|
||||
defer list.Unlock()
|
||||
list.buffer[list.end] = &WhoWas{
|
||||
nickname: client.Nick(),
|
||||
username: client.username,
|
||||
hostname: client.hostname,
|
||||
hostmask: client.hostmask,
|
||||
realname: client.realname,
|
||||
}
|
||||
list.end = (list.end + 1) % len(list.buffer)
|
||||
@@ -33,6 +42,8 @@ func (list *WhoWasList) Append(client *Client) {
|
||||
}
|
||||
|
||||
func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas {
|
||||
list.RLock()
|
||||
defer list.RUnlock()
|
||||
results := make([]*WhoWas, 0)
|
||||
for whoWas := range list.Each() {
|
||||
if nickname != whoWas.nickname {
|
||||
@@ -47,6 +58,8 @@ func (list *WhoWasList) Find(nickname Name, limit int64) []*WhoWas {
|
||||
}
|
||||
|
||||
func (list *WhoWasList) prev(index int) int {
|
||||
list.RLock()
|
||||
defer list.RUnlock()
|
||||
index -= 1
|
||||
if index < 0 {
|
||||
index += len(list.buffer)
|
||||
@@ -58,6 +71,8 @@ func (list *WhoWasList) prev(index int) int {
|
||||
func (list *WhoWasList) Each() <-chan *WhoWas {
|
||||
ch := make(chan *WhoWas)
|
||||
go func() {
|
||||
list.RLock()
|
||||
defer list.RUnlock()
|
||||
defer close(ch)
|
||||
if list.start == list.end {
|
||||
return
|
||||
|
15
ircd.yml
15
ircd.yml
@@ -1,3 +1,7 @@
|
||||
network:
|
||||
# network name
|
||||
name: Local
|
||||
|
||||
server:
|
||||
# server name
|
||||
name: localhost.localdomain
|
||||
@@ -16,7 +20,7 @@ server:
|
||||
cert: cert.pem
|
||||
|
||||
# password to login to the server
|
||||
# generated using "ircd genpasswd"
|
||||
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
|
||||
#password: ""
|
||||
|
||||
# motd filename
|
||||
@@ -27,5 +31,12 @@ operator:
|
||||
# operator named 'admin' with password 'password'
|
||||
admin:
|
||||
# password to login with /OPER command
|
||||
# generated using "ircd genpasswd"
|
||||
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
|
||||
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
|
||||
|
||||
# accounts (SASL)
|
||||
account:
|
||||
# username 'admin'
|
||||
admin:
|
||||
# password 'admin'
|
||||
password: JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD
|
||||
|
62
main.go
62
main.go
@@ -1,63 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"os"
|
||||
|
||||
"github.com/docopt/docopt-go"
|
||||
"github.com/prologic/eris/irc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/mmcloughlin/professor"
|
||||
"github.com/prologic/eris/irc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
version := irc.FullVersion()
|
||||
usage := `eris.
|
||||
Usage:
|
||||
eris genpasswd [--conf <filename>]
|
||||
eris run [--conf <filename>] [ -d | --debug ]
|
||||
eris -h | --help
|
||||
eris -v | --version
|
||||
Options:
|
||||
-c --conf <filename> Configuration file to use [default: ircd.yml].
|
||||
-h --help Show this screen.
|
||||
-v --version Show version.`
|
||||
var (
|
||||
version bool
|
||||
debug bool
|
||||
configfile string
|
||||
)
|
||||
|
||||
arguments, _ := docopt.Parse(usage, nil, true, version, false)
|
||||
flag.BoolVar(&version, "v", false, "display version information")
|
||||
flag.BoolVar(&debug, "d", false, "enable debug logging")
|
||||
flag.StringVar(&configfile, "c", "ircd.yml", "config file")
|
||||
flag.Parse()
|
||||
|
||||
if arguments["-d"].(bool) || arguments["--debug"].(bool) {
|
||||
if version {
|
||||
fmt.Printf(irc.FullVersion())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
// Special case -- We do not need to load the config file here
|
||||
if arguments["genpasswd"].(bool) {
|
||||
fmt.Print("Enter Password: ")
|
||||
bytePassword, err := terminal.ReadPassword(syscall.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal("Error reading password:", err.Error())
|
||||
}
|
||||
password := string(bytePassword)
|
||||
encoded, err := irc.GenerateEncodedPassword(password)
|
||||
if err != nil {
|
||||
log.Fatalln("encoding error:", err)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
fmt.Println(encoded)
|
||||
return
|
||||
if debug {
|
||||
go professor.Launch(":6060")
|
||||
}
|
||||
|
||||
configfile := arguments["--conf"].(string)
|
||||
config, err := irc.LoadConfig(configfile)
|
||||
if err != nil {
|
||||
log.Fatal("Config file did not load successfully:", err.Error())
|
||||
}
|
||||
|
||||
if arguments["run"].(bool) {
|
||||
server := irc.NewServer(config)
|
||||
log.Println(irc.FullVersion(), "running")
|
||||
defer log.Println(irc.FullVersion(), "exiting")
|
||||
server.Run()
|
||||
}
|
||||
irc.NewServer(config).Run()
|
||||
}
|
||||
|
577
main_test.go
Normal file
577
main_test.go
Normal file
@@ -0,0 +1,577 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/renstrom/shortuuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thoj/go-ircevent"
|
||||
|
||||
eris "github.com/prologic/eris/irc"
|
||||
)
|
||||
|
||||
const (
|
||||
TIMEOUT = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
server *eris.Server
|
||||
|
||||
debug = flag.Bool("d", false, "enable debug logging")
|
||||
)
|
||||
|
||||
func setupServer() *eris.Server {
|
||||
config := &eris.Config{}
|
||||
|
||||
config.Network.Name = "Test"
|
||||
config.Server.Name = "test"
|
||||
config.Server.Description = "Test"
|
||||
config.Server.Listen = []string{":6667"}
|
||||
|
||||
// SASL
|
||||
config.Account = map[string]*eris.PassConfig{
|
||||
"admin": &eris.PassConfig{"JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD"},
|
||||
}
|
||||
|
||||
server := eris.NewServer(config)
|
||||
|
||||
go server.Run()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func randomValidName() string {
|
||||
var name eris.Name
|
||||
for {
|
||||
name = eris.NewName(shortuuid.New())
|
||||
if name.IsNickname() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return name.String()
|
||||
}
|
||||
|
||||
func newClient(start bool) *irc.Connection {
|
||||
name := randomValidName()
|
||||
client := irc.IRC(name, name)
|
||||
client.RealName = fmt.Sprintf("Test Client: %s", name)
|
||||
|
||||
err := client.Connect("localhost:6667")
|
||||
if err != nil {
|
||||
log.Fatalf("error setting up test client: %s", err)
|
||||
}
|
||||
|
||||
if start {
|
||||
go client.Loop()
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
if *debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
|
||||
server = setupServer()
|
||||
|
||||
result := m.Run()
|
||||
|
||||
server.Stop()
|
||||
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestConnection(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client := newClient(false)
|
||||
|
||||
client.AddCallback("001", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
|
||||
defer client.Quit()
|
||||
go client.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSASL(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client := newClient(false)
|
||||
client.SASLLogin = "admin"
|
||||
client.SASLPassword = "admin"
|
||||
|
||||
client.AddCallback("001", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
|
||||
defer client.Quit()
|
||||
go client.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRplWelcome(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := "Welcome to the .* Internet Relay Network .*!.*@.*"
|
||||
actual := make(chan string)
|
||||
|
||||
client := newClient(false)
|
||||
|
||||
client.AddCallback("001", func(e *irc.Event) {
|
||||
actual <- e.Message()
|
||||
})
|
||||
|
||||
defer client.Quit()
|
||||
go client.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Regexp(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_JOIN(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := newClient(false)
|
||||
|
||||
expected := []string{client.GetNick(), "=", "#join", fmt.Sprintf("@%s", client.GetNick())}
|
||||
actual := make(chan string)
|
||||
|
||||
client.AddCallback("353", func(e *irc.Event) {
|
||||
for i := range e.Arguments {
|
||||
actual <- e.Arguments[i]
|
||||
}
|
||||
})
|
||||
|
||||
defer client.Quit()
|
||||
go client.Loop()
|
||||
|
||||
client.Join("#join")
|
||||
client.SendRaw("NAMES #join")
|
||||
|
||||
for i := range expected {
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected[i], res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_InviteOnly(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
client1.AddCallback("324", func(e *irc.Event) {
|
||||
if strings.Contains(e.Arguments[2], "i") {
|
||||
client2.Join("#inviteonly")
|
||||
} else {
|
||||
client1.Mode("#inviteonly")
|
||||
}
|
||||
})
|
||||
|
||||
client2.AddCallback("473", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
client2.AddCallback("JOIN", func(e *irc.Event) {
|
||||
actual <- false
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
client1.Join("#inviteonly")
|
||||
client1.Mode("#inviteonly", "+i")
|
||||
client1.Mode("#inviteonly")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUser_WithHostMask(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
expected := fmt.Sprintf("%x", sha256.Sum256([]byte("localhost")))
|
||||
actual := make(chan string)
|
||||
|
||||
client1.AddCallback("001", func(e *irc.Event) {
|
||||
client1.Mode(client1.GetNick(), "+x")
|
||||
})
|
||||
|
||||
client2.AddCallback("001", func(e *irc.Event) {
|
||||
client2.Whois(client1.GetNick())
|
||||
})
|
||||
|
||||
client2.AddCallback("401", func(e *irc.Event) {
|
||||
client2.Whois(client1.GetNick())
|
||||
})
|
||||
|
||||
client2.AddCallback("311", func(e *irc.Event) {
|
||||
actual <- e.Arguments[3]
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
/* FIXME: This test is racey :/
|
||||
func TestUser_WithoutHostMask(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
expected := "localhost"
|
||||
actual := make(chan string)
|
||||
|
||||
client1.AddCallback("001", func(e *irc.Event) {
|
||||
client1.Mode(client1.GetNick(), "-x")
|
||||
})
|
||||
|
||||
client2.AddCallback("001", func(e *irc.Event) {
|
||||
client2.Whois(client1.GetNick())
|
||||
})
|
||||
|
||||
client2.AddCallback("401", func(e *irc.Event) {
|
||||
client2.Whois(client1.GetNick())
|
||||
})
|
||||
|
||||
client2.AddCallback("311", func(e *irc.Event) {
|
||||
actual <- e.Arguments[3]
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func TestUser_PRIVMSG(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := "Hello World!"
|
||||
actual := make(chan string)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
client1.AddCallback("001", func(e *irc.Event) {
|
||||
client1.Privmsg(client2.GetNick(), expected)
|
||||
|
||||
})
|
||||
client1.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||
actual <- e.Message()
|
||||
})
|
||||
|
||||
client2.AddCallback("001", func(e *irc.Event) {
|
||||
client2.Privmsg(client1.GetNick(), expected)
|
||||
})
|
||||
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||
actual <- e.Message()
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_PRIVMSG(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := "Hello World!"
|
||||
actual := make(chan string)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
client1.AddCallback("JOIN", func(e *irc.Event) {
|
||||
client1.Privmsg(e.Arguments[0], expected)
|
||||
})
|
||||
client2.AddCallback("JOIN", func(e *irc.Event) {
|
||||
client2.Privmsg(e.Arguments[0], expected)
|
||||
})
|
||||
|
||||
client1.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||
actual <- e.Message()
|
||||
})
|
||||
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||
actual <- e.Message()
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
client1.Join("#channelprivmsg")
|
||||
client2.Join("#channelprivmsg")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_NoExternal(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(true)
|
||||
client2 := newClient(true)
|
||||
|
||||
client1.AddCallback("JOIN", func(e *irc.Event) {
|
||||
channel := e.Arguments[0]
|
||||
if channel == "#noexternal" {
|
||||
if e.Nick == client1.GetNick() {
|
||||
client2.Privmsg("#noexternal", "FooBar!")
|
||||
} else {
|
||||
assert.Fail(fmt.Sprintf("unexpected user %s joined %s", e.Nick, channel))
|
||||
}
|
||||
} else {
|
||||
assert.Fail(fmt.Sprintf("unexpected channel %s", channel))
|
||||
}
|
||||
})
|
||||
|
||||
client2.AddCallback("PRIVMSG", func(e *irc.Event) {
|
||||
if e.Arguments[0] == "#noexternal" {
|
||||
actual <- false
|
||||
}
|
||||
})
|
||||
client2.AddCallback("404", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
client1.Join("#noexternal")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_SetTopic_InvalidChannel(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(false)
|
||||
|
||||
client1.AddCallback("403", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
go client1.Loop()
|
||||
|
||||
client1.SendRaw("TOPIC #invalidchannel :FooBar")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_SetTopic_NotOnChannel(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
client1.AddCallback("442", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
client2.AddCallback("JOIN", func(e *irc.Event) {
|
||||
client1.SendRaw("TOPIC #notonchannel :FooBar")
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
go client1.Loop()
|
||||
|
||||
client2.Join("#notonchannel")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_BadChannelKey(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(false)
|
||||
client2 := newClient(false)
|
||||
|
||||
client1.AddCallback("324", func(e *irc.Event) {
|
||||
if strings.Contains(e.Arguments[2], "k") {
|
||||
client2.Join(e.Arguments[1])
|
||||
} else {
|
||||
client1.Mode("#badchannelkey")
|
||||
}
|
||||
})
|
||||
|
||||
client2.AddCallback("JOIN", func(e *irc.Event) {
|
||||
if e.Nick == client2.GetNick() && e.Arguments[0] == "#badchannelkey" {
|
||||
actual <- false
|
||||
}
|
||||
})
|
||||
client2.AddCallback("475", func(e *irc.Event) {
|
||||
actual <- true
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
client1.Join("#badchannelkey")
|
||||
client1.Mode("#badchannelkey", "+k", "opensesame")
|
||||
client1.Mode("#badchannelkey")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannel_GoodChannelKey(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
expected := true
|
||||
actual := make(chan bool)
|
||||
|
||||
client1 := newClient(true)
|
||||
client2 := newClient(true)
|
||||
|
||||
client1.AddCallback("324", func(e *irc.Event) {
|
||||
if strings.Contains(e.Arguments[2], "k") {
|
||||
client2.SendRawf("JOIN %s :opensesame", e.Arguments[1])
|
||||
} else {
|
||||
client1.Mode("#goodchannelkey")
|
||||
}
|
||||
})
|
||||
|
||||
client2.AddCallback("JOIN", func(e *irc.Event) {
|
||||
if e.Nick == client2.GetNick() && e.Arguments[0] == "#goodchannelkey" {
|
||||
actual <- true
|
||||
}
|
||||
})
|
||||
client2.AddCallback("475", func(e *irc.Event) {
|
||||
actual <- false
|
||||
})
|
||||
|
||||
defer client1.Quit()
|
||||
defer client2.Quit()
|
||||
go client1.Loop()
|
||||
go client2.Loop()
|
||||
|
||||
client1.Join("#goodchannelkey")
|
||||
client1.Mode("#goodchannelkey", "+k", "opensesame")
|
||||
client1.Mode("#goodchannelkey")
|
||||
|
||||
select {
|
||||
case res := <-actual:
|
||||
assert.Equal(expected, res)
|
||||
case <-time.After(TIMEOUT):
|
||||
assert.Fail("timeout")
|
||||
}
|
||||
}
|
41
scripts/release.sh
Executable file
41
scripts/release.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo -n "Version to tag: "
|
||||
read TAG
|
||||
|
||||
echo -n "Name of release: "
|
||||
read NAME
|
||||
|
||||
echo -n "Desc of release: "
|
||||
read DESC
|
||||
|
||||
git tag ${TAG}
|
||||
git push --tags
|
||||
|
||||
if [ ! -d ./bin ]; then
|
||||
mkdir bin
|
||||
else
|
||||
rm -rf ./bin/*
|
||||
fi
|
||||
|
||||
echo -n "Building binaries ... "
|
||||
|
||||
GOOS=linux GOARCH=amd64 go build -o ./bin/eris-Linux-x86_64 .
|
||||
GOOS=linux GOARCH=arm64 go build -o ./bin/eris-Linux-arm_64 .
|
||||
GOOS=darwin GOARCH=amd64 go build -o ./bin/eris-Darwin-x86_64 .
|
||||
GOOS=windows GOARCH=amd64 go build -o ./bin/eris-Windows-x86_64.exe .
|
||||
|
||||
echo "DONE"
|
||||
|
||||
echo -n "Uploading binaries ... "
|
||||
|
||||
github-release release \
|
||||
-u prologic -p -r eris \
|
||||
-t ${TAG} -n "${NAME}" -d "${DESC}"
|
||||
|
||||
for file in bin/*; do
|
||||
name="$(echo $file | sed -e 's|bin/||g')"
|
||||
github-release upload -u prologic -r eris -t ${TAG} -n $name -f $file
|
||||
done
|
||||
|
||||
echo "DONE"
|
1
vendor/github.com/DanielOaks/girc-go
generated
vendored
1
vendor/github.com/DanielOaks/girc-go
generated
vendored
Submodule vendor/github.com/DanielOaks/girc-go deleted from 3a2b80af9b
1
vendor/github.com/docopt/docopt-go
generated
vendored
1
vendor/github.com/docopt/docopt-go
generated
vendored
Submodule vendor/github.com/docopt/docopt-go deleted from 784ddc5885
1
vendor/github.com/goshuirc/e-nfa
generated
vendored
1
vendor/github.com/goshuirc/e-nfa
generated
vendored
Submodule vendor/github.com/goshuirc/e-nfa deleted from 7071788e39
1
vendor/github.com/imdario/mergo
generated
vendored
1
vendor/github.com/imdario/mergo
generated
vendored
Submodule vendor/github.com/imdario/mergo deleted from 7fe0c75c13
1
vendor/github.com/sirupsen/logrus
generated
vendored
1
vendor/github.com/sirupsen/logrus
generated
vendored
Submodule vendor/github.com/sirupsen/logrus deleted from 89742aefa4
1
vendor/golang.org/x/crypto
generated
vendored
1
vendor/golang.org/x/crypto
generated
vendored
Submodule vendor/golang.org/x/crypto deleted from 9f005a07e0
1
vendor/golang.org/x/sys
generated
vendored
1
vendor/golang.org/x/sys
generated
vendored
Submodule vendor/golang.org/x/sys deleted from bf42f188b9
1
vendor/golang.org/x/text
generated
vendored
1
vendor/golang.org/x/text
generated
vendored
Submodule vendor/golang.org/x/text deleted from 88f656faf3
1
vendor/gopkg.in/yaml.v2
generated
vendored
1
vendor/gopkg.in/yaml.v2
generated
vendored
Submodule vendor/gopkg.in/yaml.v2 deleted from eb3733d160
Reference in New Issue
Block a user