1 Commits

Author SHA1 Message Date
James Mills
d3fd6ec764 Added support for network name and RPL_WELCOME to display network name 2017-11-25 15:20:54 -08:00
36 changed files with 99 additions and 923 deletions

View File

@@ -6,15 +6,9 @@ pipeline:
build: build:
image: golang image: golang
commands: commands:
- go get -d ./... - go get -d
- go build . - go build .
test:
image: golang
commands:
- go get -d ./...
- go test ./...
docker: docker:
image: plugins/docker image: plugins/docker
repo: r.mills.io/prologic/eris repo: r.mills.io/prologic/eris

1
.gitignore vendored
View File

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

12
.gitmodules vendored
View File

@@ -43,15 +43,3 @@
[submodule "vendor/github.com/prometheus/procfs"] [submodule "vendor/github.com/prometheus/procfs"]
path = vendor/github.com/prometheus/procfs path = vendor/github.com/prometheus/procfs
url = https://github.com/prometheus/procfs url = https://github.com/prometheus/procfs
[submodule "vendor/github.com/thoj/go-ircevent"]
path = vendor/github.com/thoj/go-ircevent
url = https://github.com/thoj/go-ircevent
[submodule "vendor/github.com/stretchr/testify"]
path = vendor/github.com/stretchr/testify
url = https://github.com/stretchr/testify
[submodule "vendor/github.com/renstrom/shortuuid"]
path = vendor/github.com/renstrom/shortuuid
url = https://github.com/renstrom/shortuuid
[submodule "vendor/github.com/satori/go.uuid"]
path = vendor/github.com/satori/go.uuid
url = https://github.com/satori/go.uuid

View File

@@ -1,31 +0,0 @@
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"

View File

@@ -46,4 +46,4 @@ $ git push -u origin my-feature
When describing your bug report; please be concise and as detailed as you can 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 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. are able to provide a test case that repeatedly demonstrates the bug at hand:

View File

@@ -2,6 +2,7 @@
FROM golang:alpine AS build FROM golang:alpine AS build
ARG TAG ARG TAG
ARG BUILD
ENV APP eris ENV APP eris
ENV REPO prologic/$APP ENV REPO prologic/$APP
@@ -11,7 +12,7 @@ RUN apk add --update git make build-base && \
WORKDIR /go/src/github.com/$REPO WORKDIR /go/src/github.com/$REPO
COPY . /go/src/github.com/$REPO COPY . /go/src/github.com/$REPO
RUN make TAG=$TAG build RUN make TAG=$TAG BUILD=$BUILD build
# Runtime # Runtime
FROM alpine FROM alpine

View File

@@ -6,6 +6,7 @@ APP=eris
PACKAGE=irc PACKAGE=irc
REPO?=prologic/$(APP) REPO?=prologic/$(APP)
TAG?=latest TAG?=latest
BUILD?=-dev
all: dev all: dev
@@ -16,13 +17,13 @@ deps:
@go get ./... @go get ./...
build: clean deps build: clean deps
@echo " -> Building $(REPO) v$(TAG)-@$(COMMIT)" @echo " -> Building $(TAG)$(BUILD)"
@go build -tags "netgo static_build" -installsuffix netgo \ @go build -tags "netgo static_build" -installsuffix netgo \
-ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT) -ldflags "-w -X github.com/$(REPO)/${PACKAGE}.GitCommit=$(COMMIT) -X github.com/$(REPO)/${PACKAGE}.Build=$(BUILD)" .
@echo "Built $$(./$(APP) -v)" @echo "Built $$(./$(APP) -v)"
image: image:
@docker build --build-arg TAG=$(TAG) -t $(REPO):$(TAG) . @docker build --build-arg TAG=$(TAG) --build-arg BUILD=$(BUILD) -t $(REPO):$(TAG) .
@echo "Image created: $(REPO):$(TAG)" @echo "Image created: $(REPO):$(TAG)"
test: test:

View File

@@ -1,10 +1,4 @@
:set spell eris - IRC Server / Daemon written in Go # 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) > 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) > and much of my original contributions were made in my [fork of ergonomadic](https://github.com/prologic/ergonomadic)
@@ -25,7 +19,7 @@ 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 strife and discord. IRC is a place where you argue and get into arguments for
the sake of argument. the sake of argument.
So `eris` is an IRC daemon written from scratch in Go to facilitate discord So `eris` is an IRC daemon written from scratch in Go to factiliate discord
and have arguments for the sake of argument! and have arguments for the sake of argument!
Pull requests and issues are welcome. Pull requests and issues are welcome.
@@ -35,9 +29,9 @@ Discussion at:
* /server irc.mills.io +6697 (*use TLS/SSL*) * /server irc.mills.io +6697 (*use TLS/SSL*)
* /join #lobby * /join #lobby
Or (**not recommended**): Or (*not recommended*)P
* /server irc.mills.io (*default port 6667, non-TLS*) * /server irc.mills.io (*default port 6667, non-TLS)
* /join #lobby * /join #lobby
## Features ## Features
@@ -54,41 +48,6 @@ Or (**not recommended**):
* Simple IRC operator privileges (*overrides most things*) * Simple IRC operator privileges (*overrides most things*)
* Secure connection tracking (+z) and SecureOnly user mode (+Z) * Secure connection tracking (+z) and SecureOnly user mode (+Z)
* Secure channels (+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 ## Installation
@@ -108,14 +67,6 @@ $ go install github.com/prologic/mkpasswd
$ mkpasswd $ mkpasswd
``` ```
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 ## Deployment
To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*): To run simply run the `eris` binary (*assuming a `ircd.yml` in the current directory*):
@@ -136,19 +87,15 @@ You may want to customize the configuration however and create your own image ba
$ docker stack deploy -c docker-compose.yml eris $ 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. Which assumes a `ircd.yml` coniguration fiel int he 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 ## Related Proejcts
There are a number of supported accompanying services that are being developed alongside Eris: 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. * [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 * [Cadmus](https://github.com/prologic/cadmus) -- An IRC Bot that logs channels and provides an interface for viewing and searching logs
## Recommended Mobile clients
* [Palaver (iOS)](https://palaverapp.com/) -- SASL, TLS, Server Password, Push Notifications, IRCv3 (*Also supports custom image upload service(s) for better privacy of shared photos/images over IRC*)
## License ## License
eris is licensed under the MIT License. eris is licensed under the MIT License.

View File

@@ -155,16 +155,16 @@ func (channel *Channel) Join(client *Client, key Text) {
return return
} }
isInvited := channel.lists[InviteMask].Match(client.UserHost(false)) isInvited := channel.lists[InviteMask].Match(client.UserHost())
if !isOperator && channel.flags.Has(InviteOnly) && !isInvited { if !isOperator && channel.flags.Has(InviteOnly) && !isInvited {
client.ErrInviteOnlyChan(channel) client.ErrInviteOnlyChan(channel)
return return
} }
if channel.lists[BanMask].Match(client.UserHost(false)) && if channel.lists[BanMask].Match(client.UserHost()) &&
!isInvited && !isInvited &&
!isOperator && !isOperator &&
!channel.lists[ExceptMask].Match(client.UserHost(false)) { !channel.lists[ExceptMask].Match(client.UserHost()) {
client.ErrBannedFromChan(channel) client.ErrBannedFromChan(channel)
return return
} }
@@ -374,7 +374,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo
return channel.applyModeMask(client, change.mode, change.op, return channel.applyModeMask(client, change.mode, change.op,
NewName(change.arg)) NewName(change.arg))
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private, Secret, SecureChan: case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Private, SecureChan:
return channel.applyModeFlag(client, change.mode, change.op) return channel.applyModeFlag(client, change.mode, change.op)
case Key: case Key:
@@ -508,7 +508,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) {
} }
if channel.flags.Has(InviteOnly) { if channel.flags.Has(InviteOnly) {
channel.lists[InviteMask].Add(invitee.UserHost(false)) channel.lists[InviteMask].Add(invitee.UserHost())
} }
inviter.RplInviting(invitee, channel.name) inviter.RplInviting(invitee, channel.name)

View File

@@ -26,7 +26,6 @@ type Client struct {
hasQuit bool hasQuit bool
hops uint hops uint
hostname Name hostname Name
hostmask Name // Cloacked hostname (SHA256)
pingTime time.Time pingTime time.Time
idleTimer *time.Timer idleTimer *time.Timer
nick Name nick Name
@@ -72,17 +71,8 @@ func NewClient(server *Server, conn net.Conn) *Client {
// //
func (client *Client) writeloop() { func (client *Client) writeloop() {
for { for reply := range client.replies {
select { client.socket.Write(reply)
case reply, ok := <-client.replies:
if !ok || reply == "" || client.socket == nil {
return
}
client.socket.Write(reply)
}
if client.replies == nil {
break
}
} }
} }
@@ -93,7 +83,6 @@ func (client *Client) readloop() {
// Set the hostname for this client. // Set the hostname for this client.
client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr()) client.hostname = AddrLookupHostname(client.socket.conn.RemoteAddr())
client.hostmask = NewName(SHA256(client.hostname.String()))
for err == nil { for err == nil {
if line, err = client.socket.Read(); err != nil { if line, err = client.socket.Read(); err != nil {
@@ -128,6 +117,13 @@ func (client *Client) readloop() {
} }
func (client *Client) processCommand(cmd Command) { func (client *Client) processCommand(cmd Command) {
client.server.metrics.Counter("client", "commands").Inc()
defer func(t time.Time) {
v := client.server.metrics.SummaryVec("client", "command_duration_seconds")
v.WithLabelValues(cmd.Code().String()).Observe(time.Now().Sub(t).Seconds())
}(time.Now())
cmd.SetClient(client) cmd.SetClient(client)
if !client.registered { if !client.registered {
@@ -146,13 +142,6 @@ func (client *Client) processCommand(cmd Command) {
return return
} }
client.server.metrics.Counter("client", "commands").Inc()
defer func(t time.Time) {
v := client.server.metrics.SummaryVec("client", "command_duration_seconds")
v.WithLabelValues(cmd.Code().String()).Observe(time.Now().Sub(t).Seconds())
}(time.Now())
switch srvCmd.(type) { switch srvCmd.(type) {
case *PingCommand, *PongCommand: case *PingCommand, *PongCommand:
client.Touch() client.Touch()
@@ -218,7 +207,6 @@ func (client *Client) Register() {
return return
} }
client.registered = true client.registered = true
client.flags[HostMask] = true
client.Touch() client.Touch()
} }
@@ -232,12 +220,6 @@ func (client *Client) destroy() {
// clean up server // clean up server
if _, ok := client.socket.conn.(*tls.Conn); ok {
client.server.metrics.GaugeVec("server", "clients").WithLabelValues("secure").Dec()
} else {
client.server.metrics.GaugeVec("server", "clients").WithLabelValues("insecure").Dec()
}
client.server.connections.Dec() client.server.connections.Dec()
client.server.clients.Remove(client) client.server.clients.Remove(client)
@@ -251,7 +233,6 @@ func (client *Client) destroy() {
} }
close(client.replies) close(client.replies)
client.replies = nil
client.socket.Close() client.socket.Close()
@@ -298,14 +279,11 @@ func (c *Client) ModeString() (str string) {
return return
} }
func (c *Client) UserHost(cloacked bool) Name { func (c *Client) UserHost() Name {
username := "*" username := "*"
if c.HasUsername() { if c.HasUsername() {
username = c.username.String() 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)) return Name(fmt.Sprintf("%s!%s@%s", c.Nick(), username, c.hostname))
} }
@@ -325,7 +303,7 @@ func (c *Client) Nick() Name {
} }
func (c *Client) Id() Name { func (c *Client) Id() Name {
return c.UserHost(true) return c.UserHost()
} }
func (c *Client) String() string { func (c *Client) String() string {
@@ -368,9 +346,7 @@ func (client *Client) ChangeNickname(nickname Name) {
} }
func (client *Client) Reply(reply string) { func (client *Client) Reply(reply string) {
if client.replies != nil { client.replies <- reply
client.replies <- reply
}
} }
func (client *Client) Quit(message Text) { func (client *Client) Quit(message Text) {

View File

@@ -4,7 +4,9 @@ import (
"errors" "errors"
"regexp" "regexp"
"strings" "strings"
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
"github.com/DanielOaks/girc-go/ircmatch" "github.com/DanielOaks/girc-go/ircmatch"
) )
@@ -103,7 +105,7 @@ func (clients *ClientLookupSet) FindAll(userhost Name) *ClientSet {
var casemappedNickMask string var casemappedNickMask string
for _, client := range clients.nicks { for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String() casemappedNickMask = client.UserHost().String()
if matcher.Match(casemappedNickMask) { if matcher.Match(casemappedNickMask) {
set.Add(client) set.Add(client)
} }
@@ -121,7 +123,7 @@ func (clients *ClientLookupSet) Find(userhost Name) *Client {
var casemappedNickMask string var casemappedNickMask string
for _, client := range clients.nicks { for _, client := range clients.nicks {
casemappedNickMask = client.UserHost(false).String() casemappedNickMask = client.UserHost().String()
if matcher.Match(casemappedNickMask) { if matcher.Match(casemappedNickMask) {
return client return client
} }

View File

@@ -4,7 +4,9 @@ import (
"errors" "errors"
"io/ioutil" "io/ioutil"
"log" "log"
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
"github.com/imdario/mergo" "github.com/imdario/mergo"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"

View File

@@ -20,7 +20,6 @@ var DefObjectives = map[float64]float64{
type Metrics struct { type Metrics struct {
namespace string namespace string
metrics map[string]prometheus.Metric metrics map[string]prometheus.Metric
gaugevecs map[string]*prometheus.GaugeVec
sumvecs map[string]*prometheus.SummaryVec sumvecs map[string]*prometheus.SummaryVec
} }
@@ -28,7 +27,6 @@ func NewMetrics(namespace string) *Metrics {
return &Metrics{ return &Metrics{
namespace: namespace, namespace: namespace,
metrics: make(map[string]prometheus.Metric), metrics: make(map[string]prometheus.Metric),
gaugevecs: make(map[string]*prometheus.GaugeVec),
sumvecs: make(map[string]*prometheus.SummaryVec), sumvecs: make(map[string]*prometheus.SummaryVec),
} }
} }
@@ -103,24 +101,6 @@ func (m *Metrics) NewGaugeFunc(subsystem, name, help string, f func() float64) p
return guage return guage
} }
func (m *Metrics) NewGaugeVec(subsystem, name, help string, labels []string) *prometheus.GaugeVec {
gauagevec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: m.namespace,
Subsystem: subsystem,
Name: name,
Help: help,
},
labels,
)
key := fmt.Sprintf("%s_%s", subsystem, name)
m.gaugevecs[key] = gauagevec
prometheus.MustRegister(gauagevec)
return gauagevec
}
func (m *Metrics) NewSummary(subsystem, name, help string) prometheus.Summary { func (m *Metrics) NewSummary(subsystem, name, help string) prometheus.Summary {
summary := prometheus.NewSummary( summary := prometheus.NewSummary(
prometheus.SummaryOpts{ prometheus.SummaryOpts{
@@ -168,11 +148,6 @@ func (m *Metrics) Gauge(subsystem, name string) prometheus.Gauge {
return m.metrics[key].(prometheus.Gauge) return m.metrics[key].(prometheus.Gauge)
} }
func (m *Metrics) GaugeVec(subsystem, name string) *prometheus.GaugeVec {
key := fmt.Sprintf("%s_%s", subsystem, name)
return m.gaugevecs[key]
}
func (m *Metrics) Summary(subsystem, name string) prometheus.Summary { func (m *Metrics) Summary(subsystem, name string) prometheus.Summary {
key := fmt.Sprintf("%s_%s", subsystem, name) key := fmt.Sprintf("%s_%s", subsystem, name)
return m.metrics[key].(prometheus.Summary) return m.metrics[key].(prometheus.Summary)

View File

@@ -58,12 +58,11 @@ const (
Registered UserMode = 'r' // not a real user mode (flag) Registered UserMode = 'r' // not a real user mode (flag)
SecureConn UserMode = 'z' SecureConn UserMode = 'z'
SecureOnly UserMode = 'Z' SecureOnly UserMode = 'Z'
HostMask UserMode = 'x'
) )
var ( var (
SupportedUserModes = UserModes{ SupportedUserModes = UserModes{
Invisible, Operator, HostMask, Invisible, Operator,
} }
DefaultChannelModes = ChannelModes{ DefaultChannelModes = ChannelModes{
NoOutside, OpOnlyTopic, NoOutside, OpOnlyTopic,
@@ -71,6 +70,7 @@ var (
) )
const ( const (
Anonymous ChannelMode = 'a' // flag
BanMask ChannelMode = 'b' // arg BanMask ChannelMode = 'b' // arg
ChannelCreator ChannelMode = 'O' // flag ChannelCreator ChannelMode = 'O' // flag
ChannelOperator ChannelMode = 'o' // arg ChannelOperator ChannelMode = 'o' // arg
@@ -82,6 +82,8 @@ const (
NoOutside ChannelMode = 'n' // flag NoOutside ChannelMode = 'n' // flag
OpOnlyTopic ChannelMode = 't' // flag OpOnlyTopic ChannelMode = 't' // flag
Private ChannelMode = 'p' // flag Private ChannelMode = 'p' // flag
Quiet ChannelMode = 'q' // flag
ReOp ChannelMode = 'r' // flag
Secret ChannelMode = 's' // flag, deprecated Secret ChannelMode = 's' // flag, deprecated
UserLimit ChannelMode = 'l' // flag arg UserLimit ChannelMode = 'l' // flag arg
Voice ChannelMode = 'v' // arg Voice ChannelMode = 'v' // arg
@@ -91,7 +93,7 @@ const (
var ( var (
SupportedChannelModes = ChannelModes{ SupportedChannelModes = ChannelModes{
BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside,
OpOnlyTopic, Private, UserLimit, Secret, SecureChan, OpOnlyTopic, Private, UserLimit, SecureChan,
} }
) )
@@ -117,7 +119,7 @@ func (m *ModeCommand) HandleServer(s *Server) {
for _, change := range m.changes { for _, change := range m.changes {
switch change.mode { switch change.mode {
case Invisible, HostMask, WallOps, SecureOnly: case Invisible, WallOps, SecureOnly:
switch change.op { switch change.op {
case Add: case Add:
if target.flags[change.mode] { if target.flags[change.mode] {

View File

@@ -4,8 +4,9 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -67,7 +68,7 @@ func (store *MemoryPasswordStore) Verify(username, password string) error {
hash, ok := store.Get(username) hash, ok := store.Get(username)
if !ok { if !ok {
log.Debugf("username %s not found", username) log.Debugf("username %s not found", username)
return fmt.Errorf("account not found: %s", username) return fmt.Errorf("account not found: %S", username)
} }
return store.hasher.Compare(hash, []byte(password)) return store.hasher.Compare(hash, []byte(password))

View File

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

View File

@@ -260,16 +260,16 @@ func (target *Client) RplWhois(client *Client) {
} }
target.RplWhoisServer(client) target.RplWhoisServer(client)
target.RplWhoisLoggedIn(client) target.RplWhoisLoggedIn(client)
target.RplEndOfWhois(client) target.RplEndOfWhois()
} }
func (target *Client) RplWhoisUser(client *Client) { func (target *Client) RplWhoisUser(client *Client) {
var clientHost Name var clientHost Name
if target.flags[Operator] || !client.flags[HostMask] { if client.flags[SecureConn] {
clientHost = client.hostname clientHost = client.hostname
} else { } else {
clientHost = client.hostmask clientHost = NewName("SECURED")
} }
target.NumericReply( target.NumericReply(
@@ -324,12 +324,9 @@ func (target *Client) RplWhoisServer(client *Client) {
) )
} }
func (target *Client) RplEndOfWhois(client *Client) { func (target *Client) RplEndOfWhois() {
target.NumericReply( target.NumericReply(RPL_ENDOFWHOIS,
RPL_ENDOFWHOIS, ":End of WHOIS list")
"%s :End of WHOIS list",
client.Nick(),
)
} }
func (target *Client) RplChannelModeIs(channel *Channel) { func (target *Client) RplChannelModeIs(channel *Channel) {
@@ -340,14 +337,6 @@ func (target *Client) RplChannelModeIs(channel *Channel) {
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ] // <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
// :<hopcount> <real name> // :<hopcount> <real name>
func (target *Client) RplWhoReply(channel *Channel, client *Client) { func (target *Client) RplWhoReply(channel *Channel, client *Client) {
var clientHost Name
if target.flags[Operator] || !client.flags[HostMask] {
clientHost = client.hostname
} else {
clientHost = client.hostmask
}
channelName := "*" channelName := "*"
flags := "" flags := ""
@@ -377,18 +366,9 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) {
} }
} }
} }
target.NumericReply( target.NumericReply(RPL_WHOREPLY,
RPL_WHOREPLY, "%s %s %s %s %s %s :%d %s", channelName, client.username, client.hostname,
"%s %s %s %s %s %s :%d %s", client.server.name, client.Nick(), flags, client.hops, client.realname)
channelName,
client.username,
clientHost,
client.server.name,
client.Nick(),
flags,
client.hops,
client.realname,
)
} }
// <name> :End of WHO list // <name> :End of WHO list
@@ -489,13 +469,8 @@ func (target *Client) RplMOTDEnd() {
} }
func (target *Client) RplList(channel *Channel) { func (target *Client) RplList(channel *Channel) {
target.NumericReply( target.NumericReply(RPL_LIST,
RPL_LIST, "%s %d :%s", channel, channel.members.Count(), channel.topic)
"%s %d :%s",
channel,
channel.members.Count(),
channel.topic,
)
} }
func (target *Client) RplListEnd(server *Server) { func (target *Client) RplListEnd(server *Server) {
@@ -509,12 +484,8 @@ func (target *Client) RplNamReply(channel *Channel) {
} }
func (target *Client) RplWhoisChannels(client *Client) { func (target *Client) RplWhoisChannels(client *Client) {
target.MultilineReply( target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS,
client.WhoisChannelsNames(target), "%s :%s", client.Nick())
RPL_WHOISCHANNELS,
"%s :%s",
client.Nick(),
)
} }
func (target *Client) RplVersion() { func (target *Client) RplVersion() {
@@ -608,22 +579,9 @@ func (target *Client) RplLUserMe() {
} }
func (target *Client) RplWhoWasUser(whoWas *WhoWas) { func (target *Client) RplWhoWasUser(whoWas *WhoWas) {
var whoWasHost Name target.NumericReply(RPL_WHOWASUSER,
if target.flags[Operator] {
whoWasHost = whoWas.hostname
} else {
whoWasHost = whoWas.hostmask
}
target.NumericReply(
RPL_WHOWASUSER,
"%s %s %s * :%s", "%s %s %s * :%s",
whoWas.nickname, whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname)
whoWas.username,
whoWasHost,
whoWas.realname,
)
} }
func (target *Client) RplEndOfWhoWas(nickname Name) { func (target *Client) RplEndOfWhoWas(nickname Name) {

View File

@@ -2,7 +2,9 @@ package irc
import ( import (
"bytes" "bytes"
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
) )
type SaslState struct { type SaslState struct {

View File

@@ -44,16 +44,13 @@ type Server struct {
accounts PasswordStore accounts PasswordStore
password []byte password []byte
signals chan os.Signal signals chan os.Signal
done chan bool
whoWas *WhoWasList whoWas *WhoWasList
ids map[string]*Identity ids map[string]*Identity
} }
var ( var (
SERVER_SIGNALS = []os.Signal{ SERVER_SIGNALS = []os.Signal{syscall.SIGINT, syscall.SIGHUP,
syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT}
syscall.SIGTERM, syscall.SIGQUIT,
}
) )
func NewServer(config *Config) *Server { func NewServer(config *Config) *Server {
@@ -73,14 +70,13 @@ func NewServer(config *Config) *Server {
operators: config.Operators(), operators: config.Operators(),
accounts: NewMemoryPasswordStore(config.Accounts(), PasswordStoreOpts{}), accounts: NewMemoryPasswordStore(config.Accounts(), PasswordStoreOpts{}),
signals: make(chan os.Signal, len(SERVER_SIGNALS)), signals: make(chan os.Signal, len(SERVER_SIGNALS)),
done: make(chan bool),
whoWas: NewWhoWasList(100), whoWas: NewWhoWasList(100),
ids: make(map[string]*Identity), ids: make(map[string]*Identity),
} }
log.Debugf("accounts: %v", config.Accounts()) log.Debugf("accounts: %v", config.Accounts())
// TODO: Make this configureable? // TODO: Make this configurabel?
server.ids["global"] = NewIdentity(config.Server.Name, "global") server.ids["global"] = NewIdentity(config.Server.Name, "global")
if config.Server.Password != "" { if config.Server.Password != "" {
@@ -127,22 +123,15 @@ func NewServer(config *Config) *Server {
}, },
) )
// server registered (clients) gauge // server clients gauge
server.metrics.NewGaugeFunc( server.metrics.NewGaugeFunc(
"server", "registered", "server", "clients",
"Number of registered clients connected", "Number of registered clients connected",
func() float64 { func() float64 {
return float64(server.clients.Count()) 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 channels gauge
server.metrics.NewGaugeFunc( server.metrics.NewGaugeFunc(
"server", "channels", "server", "channels",
@@ -202,22 +191,13 @@ func (server *Server) Shutdown() {
server.Global("shutting down...") server.Global("shutting down...")
} }
func (server *Server) Stop() {
server.done <- true
}
func (server *Server) Run() { func (server *Server) Run() {
for { done := false
for !done {
select { select {
case <-server.done:
return
case <-server.signals: case <-server.signals:
server.Shutdown() server.Shutdown()
// Give at least 1s for clients to see the shutdown done = true
go func() {
time.Sleep(1 * time.Second)
server.Stop()
}()
case conn := <-server.newConns: case conn := <-server.newConns:
go NewClient(server, conn) go NewClient(server, conn)
@@ -237,12 +217,6 @@ func (s *Server) acceptor(listener net.Listener) {
} }
log.Debugf("%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.connections.Inc()
s.newConns <- conn s.newConns <- conn
} }
@@ -629,14 +603,10 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) {
} }
} }
func (client *Client) WhoisChannelsNames(target *Client) []string { func (client *Client) WhoisChannelsNames() []string {
chstrs := make([]string, client.channels.Count()) chstrs := make([]string, client.channels.Count())
index := 0 index := 0
client.channels.Range(func(channel *Channel) bool { client.channels.Range(func(channel *Channel) bool {
if !CanSeeChannel(target, channel) {
return true
}
switch { switch {
case channel.members.Get(client).Has(ChannelOperator): case channel.members.Get(client).Has(ChannelOperator):
chstrs[index] = "@" + channel.name.String() chstrs[index] = "@" + channel.name.String()
@@ -843,7 +813,7 @@ func (msg *ListCommand) HandleServer(server *Server) {
if len(msg.channels) == 0 { if len(msg.channels) == 0 {
server.channels.Range(func(name Name, channel *Channel) bool { server.channels.Range(func(name Name, channel *Channel) bool {
if !CanSeeChannel(client, channel) { if !client.flags[Operator] && channel.flags.Has(Private) {
return true return true
} }
client.RplList(channel) client.RplList(channel)
@@ -852,7 +822,7 @@ func (msg *ListCommand) HandleServer(server *Server) {
} else { } else {
for _, chname := range msg.channels { for _, chname := range msg.channels {
channel := server.channels.Get(chname) channel := server.channels.Get(chname)
if channel == nil || !CanSeeChannel(client, channel) { if channel == nil || (!client.flags[Operator] && channel.flags.Has(Private)) {
client.ErrNoSuchChannel(chname) client.ErrNoSuchChannel(chname)
continue continue
} }

View File

@@ -3,7 +3,9 @@ package irc
import ( import (
"fmt" "fmt"
"strings" "strings"
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
) )
// //

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
package irc package irc
import ( import (
"sync" //"sync"
sync "github.com/sasha-s/go-deadlock"
) )
type WhoWasList struct { type WhoWasList struct {
@@ -15,7 +17,6 @@ type WhoWas struct {
nickname Name nickname Name
username Name username Name
hostname Name hostname Name
hostmask Name
realname Text realname Text
} }
@@ -32,7 +33,6 @@ func (list *WhoWasList) Append(client *Client) {
nickname: client.Nick(), nickname: client.Nick(),
username: client.username, username: client.username,
hostname: client.hostname, hostname: client.hostname,
hostmask: client.hostmask,
realname: client.realname, realname: client.realname,
} }
list.end = (list.end + 1) % len(list.buffer) list.end = (list.end + 1) % len(list.buffer)

View File

@@ -1,575 +0,0 @@
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")
}
}
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")
}
}

View File

@@ -21,7 +21,7 @@ fi
echo -n "Building binaries ... " echo -n "Building binaries ... "
GOOS=linux GOARCH=amd64 go build -o ./bin/eris-Linux-x86_64 . 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=linux GOARCH=arm64 go build -o ./bin/eris-Linux-x86_64 .
GOOS=darwin GOARCH=amd64 go build -o ./bin/eris-Darwin-x86_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 . GOOS=windows GOARCH=amd64 go build -o ./bin/eris-Windows-x86_64.exe .