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