29 Commits

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

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

* Tweaking for channel updates

* Almost working version

* Tweaking logic for CanSpeak()

* Fixing channel flags vs client flags.HasMode()s
2017-11-23 01:13:02 -08:00
James Mills
8f269b5201 Refactored basic SASL auth 2017-11-22 20:04:26 -08:00
James Mills
d33d60353c Fixed goroutine leak for writeloop 2017-11-22 19:59:14 -08:00
James Mills
9a5862287b Updated default config to fix reference to non-existent genpasswd (use an external tool) 2017-11-20 22:54:48 -08:00
James Mills
46d22a71b3 Update README.md 2017-11-20 20:13:13 -08:00
James Mills
4d97e035d2 Fixed some SASL issues @grawity found; Thank you 2017-11-20 01:25:49 -08:00
James Mills
41b6511cec AddRPL_WHOISLOGGEDIN and +r (registered) support for SASL 2017-11-20 01:03:46 -08:00
James Mills
1cde7c6902 Add support for very basic SASL auth 2017-11-19 22:57:22 -08:00
James Mills
edfd990d59 Include package name in the version output 2017-11-19 22:57:06 -08:00
33 changed files with 828 additions and 110 deletions

View File

@@ -21,7 +21,7 @@ pipeline:
from: drone@mills.io
skip_verify: true
when:
status: [ changed, failure ]
status: [ success, changed, failure ]
secrets:
registry_username:

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*~*
bin
*.db
*.pem

6
.gitmodules vendored
View File

@@ -43,3 +43,9 @@
[submodule "vendor/github.com/prometheus/procfs"]
path = vendor/github.com/prometheus/procfs
url = https://github.com/prometheus/procfs
[submodule "vendor/github.com/sasha-s/go-deadlock"]
path = vendor/github.com/sasha-s/go-deadlock
url = https://github.com/sasha-s/go-deadlock
[submodule "vendor/github.com/petermattis/goid"]
path = vendor/github.com/petermattis/goid
url = https://github.com/petermattis/goid

8
.travis.yml Normal file
View File

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

49
CONTRIBUTING.md Normal file
View File

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

View File

@@ -6,7 +6,7 @@ APP=eris
PACKAGE=irc
REPO?=prologic/$(APP)
TAG?=latest
BUILD?=-dev
BUILD?=dev
all: dev

View File

@@ -1,5 +1,11 @@
# eris - IRC Server / Daemon written in Go
[![Build Status](https://travis-ci.org/prologic/eris.svg)](https://travis-ci.org/prologic/eris)
[![Go Report Card](https://goreportcard.com/badge/github.com/prologic/eris)](https://goreportcard.com/report/github.com/prologic/eris)
[![Coverage](https://coveralls.io/repos/prologic/eris/badge.svg)](https://coveralls.io/r/prologic/eris)
[![GoDoc](https://godoc.org/github.com/prologic/eris?status.svg)](https://godoc.org/github.com/prologic/eris)
[![Wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/prologic/eris/wiki)
> This project and repository is based off of [ergonomadic](https://github.com/edmund-huber/ergonomadic)
> and much of my original contributions were made in my [fork of ergonomadic](https://github.com/prologic/ergonomadic)
> but the upstream project was ultimately shutdown.
@@ -26,7 +32,12 @@ Pull requests and issues are welcome.
Discussion at:
* /server irc.mills.io:6697 (*use SSL*)
* /server irc.mills.io +6697 (*use TLS/SSL*)
* /join #lobby
Or (*not recommended*)P
* /server irc.mills.io (*default port 6667, non-TLS)
* /join #lobby
## Features
@@ -42,6 +53,41 @@ Discussion at:
* SSL/TLS support
* Simple IRC operator privileges (*overrides most things*)
* Secure connection tracking (+z) and SecureOnly user mode (+Z)
* Secure channels (+Z)
## 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
@@ -61,12 +107,43 @@ $ 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
$ go install github.com/prologic/mksslcert
$ mksslcert
```
## Deployment
To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*):
```#!bash
$ eris
```
Or you can deploy with [Docker](https://www.docker.com) using the prebuilt [prologic/eris](https://hub.docker.com/r/prologic/eris/):
```#!bash
docker run -d -p 6667:6667 -p 6697:6697 prologic/eris
```
You may want to customize the configuration however and create your own image based off of this; or deploy with `docker stack deploy` on a [Docker Swarm](https://docs.docker.com/engine/swarm/) clsuter like this:
```#!bash
$ docker stack deploy -c docker-compose.yml eris
```
Which assumes a `ircd.yml` coniguration file in the current directory which Docker will use to distribute as the configuration. The `docker-compose.yml` (*Docker Stackfile*) is available at the root of this repository.
## Related Proejcts
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
## License
eris is licensed under the MIT License.

View File

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

View File

@@ -155,16 +155,16 @@ func (channel *Channel) Join(client *Client, key Text) {
return
}
isInvited := channel.lists[InviteMask].Match(client.UserHost())
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
}
@@ -244,6 +244,9 @@ func (channel *Channel) CanSpeak(client *Client) bool {
channel.members.HasMode(client, ChannelOperator)) {
return false
}
if channel.flags.Has(SecureChan) && !client.flags[SecureConn] {
return false
}
return true
}
@@ -371,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, SecureChan:
return channel.applyModeFlag(client, change.mode, change.op)
case Key:
@@ -505,7 +508,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) {
}
if channel.flags.Has(InviteOnly) {
channel.lists[InviteMask].Add(invitee.UserHost())
channel.lists[InviteMask].Add(invitee.UserHost(false))
}
inviter.RplInviting(invitee, channel.name)

View File

@@ -26,12 +26,14 @@ type Client struct {
hasQuit bool
hops uint
hostname Name
hostmask Name // Cloacked hostname (SHA256)
pingTime time.Time
idleTimer *time.Timer
nick Name
quitTimer *time.Timer
realname Text
registered bool
sasl *SaslState
server *Server
socket *Socket
replies chan string
@@ -48,6 +50,7 @@ func NewClient(server *Server, conn net.Conn) *Client {
channels: NewChannelSet(),
ctime: now,
flags: make(map[UserMode]bool),
sasl: NewSaslState(),
server: server,
socket: NewSocket(conn),
replies: make(chan string),
@@ -55,7 +58,6 @@ func NewClient(server *Server, conn net.Conn) *Client {
if _, ok := conn.(*tls.Conn); ok {
client.flags[SecureConn] = true
client.flags[SecureOnly] = true
}
client.Touch()
@@ -82,6 +84,7 @@ func (client *Client) readloop() {
// Set the hostname for this client.
client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr())
client.hostmask = NewName(SHA256(client.hostname.String()))
for err == nil {
if line, err = client.socket.Read(); err != nil {
@@ -231,6 +234,8 @@ func (client *Client) destroy() {
client.quitTimer.Stop()
}
close(client.replies)
client.socket.Close()
log.Debugf("%s: destroyed", client)
@@ -276,11 +281,14 @@ func (c *Client) ModeString() (str string) {
return
}
func (c *Client) UserHost() Name {
func (c *Client) UserHost(cloacked bool) Name {
username := "*"
if c.HasUsername() {
username = c.username.String()
}
if cloacked {
return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostmask))
}
return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname))
}
@@ -300,7 +308,7 @@ func (c *Client) Nick() Name {
}
func (c *Client) Id() Name {
return c.UserHost()
return c.UserHost(true)
}
func (c *Client) String() string {

View File

@@ -105,7 +105,7 @@ func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
var casemappedNickMask string
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost().String()
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
set.Add(client)
}
@@ -123,7 +123,7 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client {
var casemappedNickMask string
for _, client := range clients.nicks {
casemappedNickMask = client.UserHost().String()
casemappedNickMask = client.UserHost(false).String()
if matcher.Match(casemappedNickMask) {
return client
}

View File

@@ -26,37 +26,38 @@ var (
NotEnoughArgsError = errors.New("not enough arguments")
ErrParseCommand = errors.New("failed to parse message")
parseCommandFuncs = map[StringCode]parseCommandFunc{
AWAY: ParseAwayCommand,
CAP: ParseCapCommand,
INVITE: ParseInviteCommand,
ISON: ParseIsOnCommand,
JOIN: ParseJoinCommand,
KICK: ParseKickCommand,
KILL: ParseKillCommand,
LIST: ParseListCommand,
MODE: ParseModeCommand,
MOTD: ParseMOTDCommand,
NAMES: ParseNamesCommand,
NICK: ParseNickCommand,
NOTICE: ParseNoticeCommand,
ONICK: ParseOperNickCommand,
OPER: ParseOperCommand,
REHASH: ParseRehashCommand,
PART: ParsePartCommand,
PASS: ParsePassCommand,
PING: ParsePingCommand,
PONG: ParsePongCommand,
PRIVMSG: ParsePrivMsgCommand,
QUIT: ParseQuitCommand,
TIME: ParseTimeCommand,
LUSERS: ParseLUsersCommand,
TOPIC: ParseTopicCommand,
USER: ParseUserCommand,
VERSION: ParseVersionCommand,
WALLOPS: ParseWallopsCommand,
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,
}
)
@@ -183,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 {

View File

@@ -33,6 +33,10 @@ type Config struct {
sync.Mutex
filename string
Network struct {
Name string
}
Server struct {
PassConfig `yaml:",inline"`
Listen []string
@@ -44,6 +48,7 @@ type Config struct {
}
Operator map[string]*PassConfig
Account map[string]*PassConfig
}
func (conf *Config) Operators() map[Name][]byte {
@@ -54,6 +59,14 @@ func (conf *Config) Operators() map[Name][]byte {
return operators
}
func (conf *Config) Accounts() map[string][]byte {
accounts := make(map[string][]byte)
for name, account := range conf.Account {
accounts[name] = []byte(account.Password)
}
return accounts
}
func (conf *Config) Name() string {
return conf.filename
}
@@ -88,6 +101,10 @@ func LoadConfig(filename string) (config *Config, err error) {
config.filename = filename
if config.Network.Name == "" {
return nil, errors.New("Network name missing")
}
if config.Server.Name == "" {
return nil, errors.New("Server name missing")
}

View File

@@ -5,38 +5,39 @@ const (
MAX_REPLY_LEN = 512 - len(CRLF)
// string codes
AWAY StringCode = "AWAY"
CAP StringCode = "CAP"
ERROR StringCode = "ERROR"
INVITE StringCode = "INVITE"
ISON StringCode = "ISON"
JOIN StringCode = "JOIN"
KICK StringCode = "KICK"
KILL StringCode = "KILL"
LIST StringCode = "LIST"
MODE StringCode = "MODE"
MOTD StringCode = "MOTD"
NAMES StringCode = "NAMES"
NICK StringCode = "NICK"
NOTICE StringCode = "NOTICE"
ONICK StringCode = "ONICK"
OPER StringCode = "OPER"
REHASH StringCode = "REHASH"
PART StringCode = "PART"
PASS StringCode = "PASS"
PING StringCode = "PING"
PONG StringCode = "PONG"
PRIVMSG StringCode = "PRIVMSG"
QUIT StringCode = "QUIT"
TIME StringCode = "TIME"
LUSERS StringCode = "LUSERS"
TOPIC StringCode = "TOPIC"
USER StringCode = "USER"
VERSION StringCode = "VERSION"
WALLOPS StringCode = "WALLOPS"
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
@@ -92,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
@@ -178,4 +180,15 @@ const (
ERR_UMODEUNKNOWNFLAG NumericCode = 501
ERR_USERSDONTMATCH NumericCode = 502
RPL_WHOISSECURE NumericCode = 671
// SASL
RPL_LOGGEDIN NumericCode = 900
RPL_LOGGEDOUT NumericCode = 901
ERR_NICKLOCKED NumericCode = 902
RPL_SASLSUCCESS NumericCode = 903
ERR_SASLFAIL NumericCode = 904
ERR_SASLTOOLONG NumericCode = 905
ERR_SASLABORTED NumericCode = 906
ERR_SASLALREADY NumericCode = 907
RPL_SASLMECHS NumericCode = 908
)

View File

@@ -55,6 +55,7 @@ const (
Invisible UserMode = 'i'
Operator UserMode = 'o'
WallOps UserMode = 'w'
Registered UserMode = 'r' // not a real user mode (flag)
SecureConn UserMode = 'z'
SecureOnly UserMode = 'Z'
)
@@ -86,12 +87,13 @@ const (
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, SecureChan,
}
)

View File

@@ -2,17 +2,124 @@ package irc
import (
"encoding/base64"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
//"sync"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus"
)
var (
EmptyPasswordError = errors.New("empty password")
)
var DefaultPasswordHasher = &Base64BCryptPasswordHasher{}
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
}
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)

View File

@@ -173,14 +173,18 @@ func RplCap(client *Client, subCommand CapSubCommand, arg interface{}) string {
// numeric replies
func (target *Client) RplWelcome() {
target.NumericReply(RPL_WELCOME,
":Welcome to the Internet Relay Network %s", target.Id())
target.NumericReply(
RPL_WELCOME,
":Welcome to the %s Internet Relay Network %s",
target.server.Network(),
target.Id(),
)
}
func (target *Client) RplYourHost() {
target.NumericReply(
RPL_YOURHOST,
":Your host is %s, running eris version %s",
":Your host is %s, running %s",
target.server.name,
FullVersion(),
)
@@ -255,13 +259,27 @@ func (target *Client) RplWhois(client *Client) {
target.RplWhoisSecure(client)
}
target.RplWhoisServer(client)
target.RplEndOfWhois()
target.RplWhoisLoggedIn(client)
target.RplEndOfWhois(client)
}
func (target *Client) RplWhoisUser(client *Client) {
target.NumericReply(RPL_WHOISUSER,
"%s %s %s * :%s", client.Nick(), client.username, client.hostname,
client.realname)
var clientHost Name
if target.flags[Operator] {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
target.NumericReply(
RPL_WHOISUSER,
"%s %s %s * :%s",
client.Nick(),
client.username,
clientHost,
client.realname,
)
}
func (target *Client) RplWhoisOperator(client *Client) {
@@ -283,6 +301,19 @@ func (target *Client) RplWhoisIdle(client *Client) {
client.Nick(), client.IdleSeconds(), client.SignonTime())
}
func (target *Client) RplWhoisLoggedIn(client *Client) {
if client.sasl.Id() == "" {
return
}
target.NumericReply(
RPL_WHOISLOGGEDIN,
"%s %s :Is logged in as",
client.Nick(),
client.sasl.Id(),
)
}
func (target *Client) RplWhoisServer(client *Client) {
target.NumericReply(
RPL_WHOISSERVER,
@@ -293,9 +324,12 @@ func (target *Client) RplWhoisServer(client *Client) {
)
}
func (target *Client) RplEndOfWhois() {
target.NumericReply(RPL_ENDOFWHOIS,
":End of WHOIS list")
func (target *Client) RplEndOfWhois(client *Client) {
target.NumericReply(
RPL_ENDOFWHOIS,
"%s :End of WHOIS list",
client.Nick(),
)
}
func (target *Client) RplChannelModeIs(channel *Channel) {
@@ -306,6 +340,14 @@ func (target *Client) RplChannelModeIs(channel *Channel) {
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
// :<hopcount> <real name>
func (target *Client) RplWhoReply(channel *Channel, client *Client) {
var clientHost Name
if target.flags[Operator] {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
channelName := "*"
flags := ""
@@ -335,9 +377,18 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) {
}
}
}
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
@@ -548,9 +599,22 @@ func (target *Client) RplLUserMe() {
}
func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
target.NumericReply(RPL_WHOWASUSER,
var whoWasHost Name
if target.flags[Operator] {
whoWasHost = whoWas.hostname
} else {
whoWasHost = whoWas.hostmask
}
target.NumericReply(
RPL_WHOWASUSER,
"%s %s %s * :%s",
whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname)
whoWas.nickname,
whoWas.username,
whoWasHost,
whoWas.realname,
)
}
func (target *Client) RplEndOfWhoWas(nickname Name) {
@@ -710,3 +774,84 @@ func (target *Client) ErrInviteOnlyChan(channel *Channel) {
target.NumericReply(ERR_INVITEONLYCHAN,
"%s :Cannot join channel (+i)", channel)
}
//
// SASL Errors / Replies
//
func RplAuthenticate(client *Client, arg string) string {
return NewStringReply(client.server, AUTHENTICATE, arg)
}
func (target *Client) RplLoggedIn(authcid string) {
target.NumericReply(
RPL_LOGGEDIN,
"%s %s :You are now logged in as %s",
target, authcid, authcid,
)
}
func (target *Client) RplLoggedOut() {
target.NumericReply(
RPL_LOGGEDIN,
"%s :You are now logged out",
target,
)
}
func (target *Client) ErrNickLocked() {
target.NumericReply(
ERR_NICKLOCKED,
"%s :You must use a nick assigned to you",
target.Nick(),
)
}
func (target *Client) RplSaslSuccess() {
target.NumericReply(
RPL_SASLSUCCESS,
"%s :SASL authentication successful",
target.Nick(),
)
}
func (target *Client) ErrSaslFail(message string) {
target.NumericReply(
ERR_SASLFAIL,
"%s :SASL authentication failed: %s",
target.Nick(), message,
)
}
func (target *Client) ErrSaslTooLong() {
target.NumericReply(
ERR_SASLFAIL,
"%s :SASL message too long",
target.Nick(),
)
}
func (target *Client) ErrSaslAborted() {
target.NumericReply(
ERR_SASLABORTED,
"%s :SASL authentication aborted",
target.Nick(),
)
}
func (target *Client) ErrSaslAlready() {
target.NumericReply(
ERR_SASLALREADY,
"%s :You have already authenticated using SASL",
target.Nick(),
)
}
func (target *Client) RplSaslMechs(mechs ...string) {
target.NumericReply(
RPL_SASLMECHS,
"%s %s :are available SASL mechanisms",
target.Nick(),
strings.Join(mechs, ","),
)
}

86
irc/sasl.go Normal file
View File

@@ -0,0 +1,86 @@
package irc
import (
"bytes"
//"sync"
sync "github.com/sasha-s/go-deadlock"
)
type SaslState struct {
sync.RWMutex
started bool
buffer *bytes.Buffer
mech string
authcid string
}
func NewSaslState() *SaslState {
return &SaslState{buffer: &bytes.Buffer{}}
}
func (s *SaslState) Reset() {
s.Lock()
defer s.Unlock()
s.started = false
s.buffer.Reset()
s.mech = ""
s.authcid = ""
}
func (s *SaslState) Started() bool {
s.RLock()
defer s.RUnlock()
return s.started
}
func (s *SaslState) Start() {
s.Lock()
defer s.Unlock()
s.started = true
}
func (s *SaslState) WriteString(data string) {
s.Lock()
defer s.Unlock()
s.buffer.WriteString(data)
}
func (s SaslState) Len() int {
s.RLock()
defer s.RUnlock()
return s.buffer.Len()
}
func (s *SaslState) String() string {
s.RLock()
defer s.RUnlock()
return s.buffer.String()
}
func (s *SaslState) Login(authcid string) {
s.Lock()
defer s.Unlock()
s.started = false
s.buffer.Reset()
s.mech = ""
s.authcid = authcid
}
func (s *SaslState) Id() string {
s.RLock()
defer s.RUnlock()
return s.authcid
}

View File

@@ -2,8 +2,10 @@ package irc
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
"os"
@@ -35,9 +37,11 @@ type Server struct {
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
whoWas *WhoWasList
@@ -60,14 +64,18 @@ func NewServer(config *Config) *Server {
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)),
whoWas: NewWhoWasList(100),
ids: make(map[string]*Identity),
}
log.Debugf("accounts: %v", config.Accounts())
// TODO: Make this configurabel?
server.ids["global"] = NewIdentity(config.Server.Name, "global")
@@ -308,6 +316,7 @@ func (s *Server) Rehash() error {
s.motdFile = s.config.Server.MOTD
s.name = NewName(s.config.Server.Name)
s.network = NewName(s.config.Network.Name)
s.description = s.config.Server.Description
s.operators = s.config.Operators()
@@ -318,6 +327,10 @@ func (s *Server) Id() Name {
return s.name
}
func (s *Server) Network() Name {
return s.network
}
func (s *Server) String() string {
return s.name.String()
}
@@ -376,6 +389,97 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
msg.setUserInfo(server)
}
func (msg *AuthenticateCommand) HandleRegServer(server *Server) {
client := msg.Client()
if !client.authorized {
client.ErrPasswdMismatch()
client.Quit("bad password")
return
}
if msg.arg == "*" {
client.ErrSaslAborted()
return
}
if !client.sasl.Started() {
if msg.arg == "PLAIN" {
client.sasl.Start()
client.Reply(RplAuthenticate(client, "+"))
} else {
client.RplSaslMechs("PLAIN")
client.ErrSaslFail("Unknown authentication mechanism")
}
return
}
if len(msg.arg) > 400 {
client.ErrSaslTooLong()
return
}
if len(msg.arg) == 400 {
client.sasl.WriteString(msg.arg)
return
}
if msg.arg != "+" {
client.sasl.WriteString(msg.arg)
}
data, err := base64.StdEncoding.DecodeString(client.sasl.String())
if err != nil {
client.ErrSaslFail("Invalid base64 encoding")
client.sasl.Reset()
return
}
// Do authentication
var (
authcid string
authzid string
password string
)
tokens := bytes.Split(data, []byte{'\000'})
if len(tokens) == 3 {
authcid = string(tokens[0])
authzid = string(tokens[1])
password = string(tokens[2])
if authzid == "" {
authzid = authcid
} else if authzid != authcid {
client.ErrSaslFail("authzid and authcid should be the same")
return
}
} else {
client.ErrSaslFail("invalid authentication blob")
return
}
err = server.accounts.Verify(authcid, password)
if err != nil {
client.ErrSaslFail("invalid authentication")
return
}
client.sasl.Login(authcid)
client.RplLoggedIn(authcid)
client.RplSaslSuccess()
client.flags[Registered] = true
client.Reply(
RplModeChanges(
client, client,
ModeChanges{
&ModeChange{mode: Registered, op: Add},
},
),
)
}
func (msg *UserCommand) setUserInfo(server *Server) {
client := msg.Client()

11
irc/utils.go Normal file
View File

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

View File

@@ -1,11 +1,18 @@
package irc
import (
"fmt"
)
var (
//PackageName package name
Package = "eris"
// Version release version
Version = "1.6.0"
Version = "1.6.2"
// Build will be overwritten automatically by the build system
Build = "-dev"
Build = "dev"
// GitCommit will be overwritten automatically by the build system
GitCommit = "HEAD"
@@ -13,5 +20,5 @@ var (
// FullVersion display the full version and build
func FullVersion() string {
return Version + Build + " (" + GitCommit + ")"
return fmt.Sprintf("%s-%s-%s@%s", Package, Version, Build, GitCommit)
}

View File

@@ -17,6 +17,7 @@ type WhoWas struct {
nickname Name
username Name
hostname Name
hostmask Name
realname Text
}
@@ -33,6 +34,7 @@ func (list *WhoWasList) Append(client *Client) {
nickname: client.Nick(),
username: client.username,
hostname: client.hostname,
hostmask: client.hostmask,
realname: client.realname,
}
list.end = (list.end + 1) % len(list.buffer)

View File

@@ -1,3 +1,7 @@
network:
# network name
name: Local
server:
# server name
name: localhost.localdomain
@@ -16,7 +20,7 @@ server:
cert: cert.pem
# password to login to the server
# generated using "ircd genpasswd"
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
#password: ""
# motd filename
@@ -27,5 +31,12 @@ operator:
# operator named 'admin' with password 'password'
admin:
# password to login with /OPER command
# generated using "ircd genpasswd"
# generated using "mkpasswd" (from https://github.com/prologic/mkpasswd)
password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu
# accounts (SASL)
account:
# username 'admin'
admin:
# password 'admin'
password: JDJhJDA0JGtUU1JVc1JOUy9DbEh1WEdvYVlMdGVnclp6YnA3NDBOZGY1WUZhdTZtRzVmb1VKdXQ5ckZD

View File

@@ -23,7 +23,7 @@ func main() {
flag.Parse()
if version {
fmt.Printf("eris v%s", irc.FullVersion())
fmt.Printf(irc.FullVersion())
os.Exit(0)
}

41
scripts/release.sh Executable file
View File

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

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