Merge remote-tracking branch 'github/master'

This commit is contained in:
hybris 2021-12-29 16:19:22 +01:00
commit b7a5c66929
21 changed files with 1141 additions and 157 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
**/*
!.git/
!*.go
!GNUmakefile
!common.mk
!Dockerfile
!.dockerignore

View File

@ -1,3 +1,4 @@
* Sergey Matveev <stargrave@stargrave.org>
* Thomas Habets <thomas@habets.se>
* Björn Busse <mail@baerlin.eu>
* Björn Busse <bj.rn@baerlin.eu>
* steigr <me@steigr>

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM golang:1.10 AS goircd-builder
ARG PACKAGE=github.com/bbusse/goircd
ENV PACKAGE=$PACKAGE
WORKDIR /go/src/$PACKAGE/
ADD . /go/src/$PACKAGE/
RUN export CGO_ENABLED=0 \
&& go get $PACKAGE \
&& make -f GNUmakefile goircd \
&& mv goircd /go/bin/goircd
FROM alpine AS goircd
COPY --from=goircd-builder /go/bin/goircd /bin/goircd
ENTRYPOINT ["goircd"]

4
GNUmakefile Normal file
View File

@ -0,0 +1,4 @@
VERSION = $(shell git describe --tags)
PACKAGE ?= quay.io/goircd/goircd
include common.mk

21
charts/goircd/.helmignore Normal file
View File

@ -0,0 +1,21 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

5
charts/goircd/Chart.yaml Normal file
View File

@ -0,0 +1,5 @@
apiVersion: v1
appVersion: "1.0"
description: minimalistic simple Internet Relay Chat (IRC) server
name: goircd
version: 0.1.0

View File

@ -0,0 +1,15 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "goircd.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get svc -w {{ template "goircd.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "goircd.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "goircd.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit irc://127.0.0.1:6667 to use your application"
kubectl port-forward $POD_NAME 6667:6667
{{- end }}

View File

@ -0,0 +1,32 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "goircd.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "goircd.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "goircd.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "goircd.fullname" . }}
data:
BIND: ":{{ .Values.service.internalPort }}"
HEALTHCHECK: {{ .Values.config.healthcheck | quote }}
HOSTNAME: {{ .Values.config.hostname | quote }}
METRICS: {{ .Values.config.metrics | quote }}
MOTD: |
{{ .Values.config.motd }}

View File

@ -0,0 +1,67 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ template "goircd.fullname" . }}
labels:
app: {{ template "goircd.name" . }}
chart: {{ template "goircd.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "goircd.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "goircd.name" . }}
release: {{ .Release.Name }}
spec:
volumes:
- name: config
configMap:
name: {{ template "goircd.fullname" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- -motd
- /config/MOTD
envFrom:
- configMapRef:
name: {{ template "goircd.fullname" . }}
volumeMounts:
- name: config
mountPath: /config
ports:
- name: irc
containerPort: {{ .Values.service.internalPort }}
protocol: TCP
- name: health
containerPort: {{ .Values.image.healthcheckPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /live
port: health
readinessProbe:
httpGet:
path: /ready
port: health
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}

View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "goircd.fullname" . }}
labels:
app: {{ template "goircd.name" . }}
chart: {{ template "goircd.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: irc
protocol: TCP
name: irc
selector:
app: {{ template "goircd.name" . }}
release: {{ .Release.Name }}

37
charts/goircd/values.yaml Normal file
View File

@ -0,0 +1,37 @@
# Default values for goircd.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
config:
healthcheck: true
hostname: irc.example.com
metrics: true
motd: |
Hello kubernetes with helm
image:
repository: quay.io/goircd/goircd
tag: latest
pullPolicy: IfNotPresent
healthcheckPort: 8086
service:
type: ClusterIP
internalPort: 6667
externalPort: 6967
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -1,6 +1,10 @@
/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
<<<<<<< HEAD
Copyright (C) 2014-2021 Sergey Matveev <stargrave@stargrave.org>
=======
Copyright (C) 2014-2016 Sergey Matveev <stargrave@stargrave.org>
>>>>>>> github/master
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -57,8 +61,9 @@ type Client struct {
away string
recvTimestamp time.Time
sendTimestamp time.Time
outBuf chan string
outBuf chan *string
alive bool
quitMsg *string
sync.Mutex
}
@ -80,15 +85,23 @@ func (c *Client) String() string {
return strings.Join([]string{c.nickname, "!", c.username, "@", c.Host()}, "")
}
func (c *Client) Match(other string) bool {
return strings.ToLower(*c.nickname) == strings.ToLower(other)
}
func NewClient(conn net.Conn, events chan ClientEvent) *Client {
nickname := "*"
username := ""
realname := ""
c := Client{
conn: conn,
nickname: "*",
username: "",
nickname: &nickname,
username: &username,
realname: &realname,
recvTimestamp: time.Now(),
sendTimestamp: time.Now(),
alive: true,
outBuf: make(chan string, MaxOutBuf),
outBuf: make(chan *string, MaxOutBuf),
}
clientsWG.Add(2)
go c.MsgSender()
@ -96,11 +109,16 @@ func NewClient(conn net.Conn, events chan ClientEvent) *Client {
return &c
}
func (c *Client) Close() {
func (c *Client) SetDead() {
c.outBuf <- nil
c.alive = false
}
func (c *Client) Close(text string) {
c.Lock()
if c.alive {
close(c.outBuf)
c.alive = false
c.quitMsg = &text
c.SetDead()
}
c.Unlock()
}
@ -147,6 +165,7 @@ func (c *Client) Processor(events chan ClientEvent) {
}
events <- ClientEvent{c, EventDel, ""}
clientsWG.Done()
sink <- ClientEvent{c, EventDel, *c.quitMsg}
}
func (c *Client) MsgSender() {
@ -155,6 +174,10 @@ func (c *Client) MsgSender() {
if *debug {
log.Println(c, "<-", msg)
}
if msg == nil {
c.conn.Close()
break
}
if _, err = c.conn.Write(append([]byte(msg), CRLF...)); err != nil {
if *verbose {
log.Println(c, "error writing", err)
@ -177,10 +200,11 @@ func (c *Client) Msg(text string) {
if c.alive {
close(c.outBuf)
c.alive = false
c.SetDead()
}
return
}
c.outBuf <- text
c.outBuf <- &text
}
func (c *Client) Reply(text string) {

18
common.mk Normal file
View File

@ -0,0 +1,18 @@
LDFLAGS = -X main.version=$(VERSION)
goircd: *.go
go build -ldflags "$(LDFLAGS)"
docker-image: *.go Dockerfile .dockerignore
docker build -t $(shell basename $(PACKAGE)):$(VERSION) .
docker-image-push: docker-image-push-latest docker-image-push-version
@true
docker-image-push-version: docker-image-push-latest docker-image-push-version
docker tag $(shell basename $(PACKAGE)):$(VERSION) $(PACKAGE):$(VERSION)
docker push $(PACKAGE):$(VERSION)
docker-image-push-latest: docker-image
docker tag $(shell basename $(PACKAGE)):$(VERSION) $(PACKAGE):latest
docker push $(PACKAGE):latest

428
daemon.go
View File

@ -19,9 +19,16 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
const (
@ -31,15 +38,327 @@ const (
PingThreshold = time.Second * 90
)
var (
RENickname = regexp.MustCompile("^[^\\x00\\x0D\\x0A\\x20\\x3A]{1,64}$") // any octet except NUL, CR, LF, " " and ":"
clients map[*Client]struct{} = make(map[*Client]struct{})
clientsM sync.RWMutex
rooms map[string]*Room = make(map[string]*Room)
roomsM sync.RWMutex
roomsGroup sync.WaitGroup
roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent)
)
func GetRoom(name string) (r *Room, found bool) {
var room string
if strings.HasPrefix(name, "#") {
room = strings.ToLower(name)
} else {
room = "#" + strings.ToLower(name)
}
r, found = rooms[room]
return r, found
}
func GetNumberOfRegisteredUsers(client *Client) (nusers float64) {
nusers = 0
clientsM.RLock()
for client := range clients {
if client.registered {
nusers++
}
}
clientsM.RUnlock()
return nusers
}
func SendLusers(client *Client) {
lusers := int(GetNumberOfRegisteredUsers(client))
client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers))
}
func SendMotd(client *Client) {
if motd == nil {
client.ReplyNicknamed("422", "MOTD File is missing")
return
}
motdText, err := ioutil.ReadFile(*motd)
if err != nil {
log.Printf("Can not read motd file %s: %v", *motd, err)
client.ReplyNicknamed("422", "Error reading MOTD File")
return
}
client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -")
for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") {
client.ReplyNicknamed("372", "- "+s)
}
client.ReplyNicknamed("376", "End of /MOTD command")
}
func SendWhois(client *Client, nicknames []string) {
var c *Client
var hostPort string
var err error
var subscriptions []string
var room *Room
var subscriber *Client
for _, nickname := range nicknames {
clientsM.RLock()
for c = range clients {
if c.Match(nickname) {
goto Found
}
}
clientsM.RUnlock()
client.ReplyNoNickChan(nickname)
continue
Found:
hostPort, _, err = net.SplitHostPort(c.conn.RemoteAddr().String())
if err != nil {
log.Printf("Can't parse RemoteAddr %q: %v", hostPort, err)
hostPort = "Unknown"
}
client.ReplyNicknamed("311", *c.nickname, *c.username, hostPort, "*", *c.realname)
client.ReplyNicknamed("312", *c.nickname, *hostname, *hostname)
if c.away != nil {
client.ReplyNicknamed("301", *c.nickname, *c.away)
}
subscriptions = make([]string, 0)
roomsM.RLock()
for _, room = range rooms {
for subscriber = range room.members {
if subscriber.Match(nickname) {
subscriptions = append(subscriptions, *room.name)
}
}
}
roomsM.RUnlock()
sort.Strings(subscriptions)
client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " "))
client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list")
clientsM.RUnlock()
}
}
func SendList(client *Client, cols []string) {
var rs []string
var r string
if (len(cols) > 1) && (cols[1] != "") {
rs = strings.Split(strings.Split(cols[1], " ")[0], ",")
} else {
rs = make([]string, 0)
roomsM.RLock()
for r = range rooms {
rs = append(rs, r)
}
roomsM.RUnlock()
}
sort.Strings(rs)
var room *Room
var found bool
for _, r = range rs {
roomsM.RLock()
if room, found = rooms[r]; found {
client.ReplyNicknamed(
"322",
*room.name,
fmt.Sprintf("%d", len(room.members)),
*room.topic,
)
}
roomsM.RUnlock()
}
client.ReplyNicknamed("323", "End of /LIST")
}
func ClientNick(client *Client, cols []string) {
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyParts("431", "No nickname given")
return
}
nickname := cols[1]
// Compatibility with some clients prepending colons to nickname
nickname = strings.TrimPrefix(nickname, ":")
rename := false
clientsM.RLock()
for existingClient := range clients {
if existingClient == client {
rename = true
} else if existingClient.Match(nickname) {
clientsM.RUnlock()
client.ReplyParts("433", "*", nickname, "Nickname is already in use")
return
}
}
clientsM.RUnlock()
if !RENickname.MatchString(nickname) {
client.ReplyParts("432", "*", cols[1], "Erroneous nickname")
return
}
if rename {
// find all rooms the client has subscribed,
// then gather all clients in those rooms
cs := make(map[*Client]struct{})
clientsM.RLock() // first clients, then rooms,
roomsM.RLock() // to avoid deadlock with SendWhois
for _, r := range rooms {
if _, subscribed := r.members[client]; subscribed {
for c := range r.members {
cs[c] = struct{}{}
}
}
}
// then notify those clients of the nick change
message := ":" + client.String() + " NICK " + nickname
for c := range cs {
c.Msg(message)
}
roomsM.RUnlock()
clientsM.RUnlock()
}
client.nickname = &nickname
}
// Unregistered client workflow processor. Unregistered client:
// * is not PINGed
// * only QUIT, NICK and USER commands are processed
// * other commands are quietly ignored
// When client finishes NICK/USER workflow, then MOTD and LUSERS are send to him.
func ClientRegister(client *Client, cmd string, cols []string) {
switch cmd {
case "PASS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("PASS")
return
}
password := strings.TrimPrefix(cols[1], ":")
client.password = &password
case "NICK":
ClientNick(client, cols)
case "USER":
if len(cols) == 1 {
client.ReplyNotEnoughParameters("USER")
return
}
args := strings.SplitN(cols[1], " ", 4)
if len(args) < 4 {
client.ReplyNotEnoughParameters("USER")
return
}
client.username = &args[0]
realname := strings.TrimLeft(args[3], ":")
client.realname = &realname
}
if *client.nickname != "*" && *client.username != "" {
if passwords != nil && *passwords != "" {
if client.password == nil {
client.ReplyParts("462", "You may not register")
client.Close("462")
return
}
contents, err := ioutil.ReadFile(*passwords)
if err != nil {
log.Fatalf("Can no read passwords file %s: %s", *passwords, err)
return
}
for _, entry := range strings.Split(string(contents), "\n") {
if entry == "" {
continue
}
if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password {
client.ReplyParts("462", "You may not register")
client.Close("462")
return
}
}
}
client.registered = true
clients_irc_total.Inc()
clients_connected.Set(GetNumberOfRegisteredUsers(client))
client.ReplyNicknamed("001", "Hi, welcome to IRC")
client.ReplyNicknamed("002", "Your host is "+*hostname+", running goircd "+version)
client.ReplyNicknamed("003", "This server was created sometime")
client.ReplyNicknamed("004", *hostname+" goircd o o")
SendLusers(client)
SendMotd(client)
log.Println(client, "logged in")
}
}
// Register new room in Daemon. Create an object, events sink, save pointers
// to corresponding daemon's places and start room's processor goroutine.
func RoomRegister(name string) (*Room, chan ClientEvent) {
roomNew := NewRoom(name)
roomSink := make(chan ClientEvent)
roomsM.Lock()
rooms[strings.ToLower(name)] = roomNew
roomSinks[roomNew] = roomSink
roomsM.Unlock()
go roomNew.Processor(roomSink)
roomsGroup.Add(1)
return roomNew, roomSink
}
func HandlerJoin(client *Client, cmd string) {
args := strings.Split(cmd, " ")
rs := strings.Split(args[0], ",")
var keys []string
if len(args) > 1 {
keys = strings.Split(args[1], ",")
} else {
keys = make([]string, 0)
}
var roomExisting *Room
var roomSink chan ClientEvent
var roomNew *Room
for n, room := range rs {
if !RoomNameValid(room) {
client.ReplyNoChannel(room)
continue
}
var key string
if (n < len(keys)) && (keys[n] != "") {
key = keys[n]
} else {
key = ""
}
roomsM.RLock()
for roomExisting, roomSink = range roomSinks {
if roomExisting.Match(room) {
roomsM.RUnlock()
if (*roomExisting.key != "") && (*roomExisting.key != key) {
goto Denied
}
roomSink <- ClientEvent{client, EventNew, ""}
goto Joined
}
}
roomsM.RUnlock()
roomNew, roomSink = RoomRegister(room)
log.Println("Room", roomNew, "created")
if key != "" {
roomNew.key = &key
roomNew.StateSave()
}
roomSink <- ClientEvent{client, EventNew, ""}
continue
Denied:
client.ReplyNicknamed("475", room, "Cannot join channel (+k) - bad key")
Joined:
clients_irc_rooms_total.With(prometheus.Labels{"room": "all"}).Inc()
clients_irc_rooms_total.With(prometheus.Labels{"room": room}).Inc()
}
}
func Processor(events chan ClientEvent, finished chan struct{}) {
var now time.Time
ticker := time.NewTicker(10 * time.Second)
go func() {
for range ticker.C {
events <- ClientEvent{eventType: EventTick}
}
}()
EventsCycle:
for e := range events {
now = time.Now()
client := e.client
@ -101,10 +420,14 @@ EventsCycle:
log.Println(client, "command", cmd)
}
if cmd == "QUIT" {
client.Close()
if *verbose {
log.Println(client, "quit")
var quitMsg string
if len(cols) >= 2 {
quitMsg = strings.TrimPrefix(cols[1], ":")
} else {
quitMsg = *client.nickname
}
client.Close(quitMsg)
continue
}
if !client.registered {
@ -121,14 +444,17 @@ EventsCycle:
client.ReplyNicknamed("305", "You are no longer marked as being away")
continue
}
client.away = strings.TrimLeft(cols[1], ":")
msg := cols[1]
client.away = &msg
client.ReplyNicknamed("306", "You have been marked as being away")
case "JOIN":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("JOIN")
continue
}
client.Join(cols[1])
HandlerJoin(client, cols[1])
case "NICK":
ClientNick(client, cols)
case "LIST":
client.SendList(cols)
case "LUSERS":
@ -139,15 +465,20 @@ EventsCycle:
continue
}
cols = strings.SplitN(cols[1], " ", 2)
if cols[0] == client.username {
client.Msg("221 " + client.nickname + " +w")
// client.ReplyNicknamed("501", "Unknown MODE flag")
if strings.ToLower(cols[0]) == strings.ToLower(*client.username) {
if len(cols) == 1 {
client.Msg("221 " + *client.nickname + " +")
} else {
client.ReplyNicknamed("501", "Unknown MODE flag")
}
continue
}
room := cols[0]
r, found := rooms[room]
roomsM.RLock()
r, found := GetRoom(room)
if !found {
client.ReplyNoChannel(room)
roomsM.RUnlock()
continue
}
if len(cols) == 1 {
@ -155,6 +486,7 @@ EventsCycle:
} else {
r.events <- ClientEvent{client, EventMode, cols[1]}
}
roomsM.RUnlock()
case "MOTD":
client.SendMotd()
case "NAMES":
@ -184,16 +516,22 @@ EventsCycle:
client.ReplyNotEnoughParameters("PART")
continue
}
rs := strings.Split(cols[1], " ")[0]
roomsLock.RLock()
for _, room := range strings.Split(rs, ",") {
if r, found := rooms[room]; found {
r.events <- ClientEvent{client, EventDel, ""}
rs := strings.SplitN(cols[1], " ", 2)
roomsM.RLock()
for _, room := range strings.Split(rs[0], ",") {
if r, found := GetRoom(room); found {
var partMsg string
if len(rs) >= 2 {
partMsg = strings.TrimPrefix(rs[1], ":")
} else {
partMsg = *client.nickname
}
roomSinks[r] <- ClientEvent{client, EventDel, partMsg}
} else {
client.ReplyNoChannel(room)
}
}
roomsLock.RUnlock()
roomsM.RUnlock()
case "PING":
if len(cols) == 1 {
client.ReplyNicknamed("409", "No origin specified")
@ -212,10 +550,26 @@ EventsCycle:
client.ReplyNicknamed("412", "No text to send")
continue
}
target := strings.ToLower(cols[0])
roomsLock.RLock()
if r, found := rooms[target]; found {
r.events <- ClientEvent{
msg := ""
target := cols[0]
clientsM.RLock()
for c := range clients {
if c.Match(target) {
msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1])
c.Msg(msg)
if c.away != nil {
client.ReplyNicknamed("301", *c.nickname, *c.away)
}
break
}
}
clientsM.RUnlock()
if msg != "" {
continue
}
roomsM.RLock()
if r, found := rooms[strings.ToLower(target)]; found {
roomSinks[r] <- ClientEvent{
client,
EventMsg,
cmd + " " + strings.TrimLeft(cols[1], ":"),
@ -242,13 +596,16 @@ EventsCycle:
continue
}
client.ReplyNoNickChan(target)
roomsM.RUnlock()
case "TOPIC":
if len(cols) == 1 {
client.ReplyNotEnoughParameters("TOPIC")
continue
}
cols = strings.SplitN(cols[1], " ", 2)
r, found := rooms[cols[0]]
roomsM.RLock()
r, found := GetRoom(cols[0])
roomsM.RUnlock()
if !found {
client.ReplyNoChannel(cols[0])
continue
@ -257,19 +614,22 @@ EventsCycle:
if len(cols) > 1 {
change = cols[1]
}
r.events <- ClientEvent{client, EventTopic, change}
roomsM.RLock()
roomSinks[r] <- ClientEvent{client, EventTopic, change}
roomsM.RUnlock()
case "WHO":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHO")
continue
}
room := strings.Split(cols[1], " ")[0]
r, found := rooms[room]
if found {
r.events <- ClientEvent{client, EventWho, ""}
roomsM.RLock()
if r, found := GetRoom(room); found {
roomSinks[r] <- ClientEvent{client, EventWho, ""}
} else {
client.ReplyNoChannel(room)
}
roomsM.RUnlock()
case "WHOIS":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHOIS")
@ -277,7 +637,7 @@ EventsCycle:
}
cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",")
client.SendWhois(nicknames)
SendWhois(client, nicknames)
case "ISON":
if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("ISON")
@ -288,13 +648,13 @@ EventsCycle:
for _, nickname := range nicknamesList {
nicknames[nickname] = false
}
clientsLock.RLock()
clientsM.RLock()
for c := range clients {
if _, exists := nicknames[c.nickname]; exists {
nicknames[c.nickname] = true
}
}
clientsLock.RUnlock()
clientsM.RUnlock()
nicknamesList = nicknamesList[:0]
for n, exists := range nicknames {
if exists {
@ -318,6 +678,17 @@ EventsCycle:
for _, c := range cs {
c.Msg(fmt.Sprintf(":%s NOTICE %s %s", client, c.nickname, cols[1]))
}
clientsM.RLock()
var nicksExists []string
for _, nickname := range strings.Split(cols[1], " ") {
for c := range clients {
if c.Match(nickname) {
nicksExists = append(nicksExists, nickname)
}
}
}
clientsM.RUnlock()
client.ReplyNicknamed("303", strings.Join(nicksExists, " "))
case "VERSION":
var debug string
if *verbose {
@ -345,6 +716,7 @@ EventsCycle:
// Read their EventDel
go func() {
for range events {
clients_connected.Set(GetNumberOfRegisteredUsers(client))
}
}()

120
events.go Normal file
View File

@ -0,0 +1,120 @@
/*
goircd -- minimalistic simple Internet Relay Chat (IRC) server
Copyright (C) 2014-2016 Sergey Matveev <stargrave@stargrave.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"time"
)
const (
EventNew = iota
EventDel = iota
EventMsg = iota
EventTopic = iota
EventWho = iota
EventMode = iota
EventTerm = iota
EventTick = iota
FormatMsg = "[%s] <%s> %s\n"
FormatMeta = "[%s] * %s %s\n"
)
var (
logSink chan LogEvent = make(chan LogEvent)
stateSink chan StateEvent = make(chan StateEvent)
)
// Client events going from each of client
// They can be either NEW, DEL or unparsed MSG
type ClientEvent struct {
client *Client
eventType int
text string
}
func (m ClientEvent) String() string {
return string(m.eventType) + ": " + m.client.String() + ": " + m.text
}
// Logging in-room events
// Intended to tell when, where and who send a message or meta command
type LogEvent struct {
where string
who string
what string
meta bool
}
// Logging events logger itself
// Each room's events are written to separate file in logdir
// Events include messages, topic and keys changes, joining and leaving
func Logger(logdir string, events <-chan LogEvent) {
mode := os.O_CREATE | os.O_WRONLY | os.O_APPEND
perm := os.FileMode(0660)
var format string
var logfile string
var fd *os.File
var err error
for event := range events {
logfile = path.Join(logdir, event.where+".log")
fd, err = os.OpenFile(logfile, mode, perm)
if err != nil {
log.Println("Can not open logfile", logfile, err)
continue
}
if event.meta {
format = FormatMeta
} else {
format = FormatMsg
}
_, err = fd.WriteString(fmt.Sprintf(format, time.Now(), event.who, event.what))
fd.Close()
if err != nil {
log.Println("Error writing to logfile", logfile, err)
}
}
}
type StateEvent struct {
where string
topic string
key string
}
// Room state events saver
// Room states shows that either topic or key has been changed
// Each room's state is written to separate file in statedir
func StateKeeper(statedir string, events <-chan StateEvent) {
var fn string
var data string
var err error
for event := range events {
fn = path.Join(statedir, event.where)
data = event.topic + "\n" + event.key + "\n"
err = ioutil.WriteFile(fn, []byte(data), os.FileMode(0660))
if err != nil {
log.Printf("Can not write statefile %s: %v", fn, err)
}
}
}

View File

@ -0,0 +1,13 @@
global
daemon
maxconn 4
defaults
mode tcp
timeout server 3600
timeout client 3600
timeout connect 5
listen ircd-demo
bind *:9667
server goircd-pv2 127.0.0.1:6667 send-proxy-v2

View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
[[ -f examples/proxy-protocol/haproxy.pid ]] && rm examples/proxy-protocol/haproxy.pid
fail() { echo "$*"; exit 1; }
set -x
which haproxy || fail haproxy is missing
which socat || fail socat is missing
test -f goircd || fail goircd is missing
haproxy_cfg() {
cat<<__cfg
global
daemon
maxconn 4
defaults
mode tcp
timeout server 3600
timeout client 3600
timeout connect 5
listen ircd-demo
bind *:9667
server goircd-pv2 127.0.0.1:6667 $1
__cfg
}
./goircd &
trap "kill $!" EXIT
# direct connect
haproxy_cfg > examples/proxy-protocol/haproxy.cfg
haproxy -f examples/proxy-protocol/haproxy.cfg -c
haproxy -f examples/proxy-protocol/haproxy.cfg -p examples/proxy-protocol/haproxy.pid
(sleep 1; echo "CONNECT" ) | socat stdin tcp:127.0.0.1:9667
kill $(cat examples/proxy-protocol/haproxy.pid)
# proxy v1 protocol
haproxy_cfg send-proxy > examples/proxy-protocol/haproxy.cfg
haproxy -f examples/proxy-protocol/haproxy.cfg -c
haproxy -f examples/proxy-protocol/haproxy.cfg -p examples/proxy-protocol/haproxy.pid
(sleep 1; echo "CONNECT" ) | socat stdin tcp:127.0.0.1:9667
kill $(cat examples/proxy-protocol/haproxy.pid)
# proxy v2 protocol
haproxy_cfg send-proxy-v2 > examples/proxy-protocol/haproxy.cfg
haproxy -f examples/proxy-protocol/haproxy.cfg -c
haproxy -f examples/proxy-protocol/haproxy.cfg -p examples/proxy-protocol/haproxy.pid
(sleep 1; echo "CONNECT" ) | socat stdin tcp:127.0.0.1:9667
kill $(cat examples/proxy-protocol/haproxy.pid)
rm examples/proxy-protocol/haproxy.pid

157
goircd.go
View File

@ -19,17 +19,27 @@ package main
import (
"crypto/tls"
"flag"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
healthchecking "github.com/heptiolabs/healthcheck"
"github.com/namsral/flag"
proxyproto "github.com/Freeaqingme/go-proxyproto"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
PROXY_TIMEOUT = 5
)
const (
@ -61,27 +71,51 @@ type StateEvent struct {
}
var (
hostname = flag.String("hostname", "localhost", "hostname")
bind = flag.String("bind", "[::1]:6667", "address to bind to")
cloak = flag.String("cloak", "", "cloak user's host with the given hostname")
motd = flag.String("motd", "", "path to MOTD file")
logdir = flag.String("logdir", "", "absolute path to directory for logs")
statedir = flag.String("statedir", "", "absolute path to directory for states")
passwords = flag.String("passwd", "", "optional path to passwords file")
version string
hostname = flag.String("hostname", "localhost", "Hostname")
bind = flag.String("bind", ":6667", "Address to bind to")
motd = flag.String("motd", "", "Path to MOTD file")
logdir = flag.String("logdir", "", "Absolute path to directory for logs")
statedir = flag.String("statedir", "", "Absolute path to directory for states")
passwords = flag.String("passwords", "", "Optional path to passwords file")
tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
tlsPEM = flag.String("tlspem", "", "path to TLS certificat+key PEM file")
permStateDirS = flag.String("perm-state-dir", "755", "state directory permissions")
permStateFileS = flag.String("perm-state-file", "600", "state files permissions")
permLogFileS = flag.String("perm-log-file", "644", "log files permissions")
timestamped = flag.Bool("timestamped", false, "enable timestamps on stderr messages")
verbose = flag.Bool("verbose", false, "enable verbose logging")
debug = flag.Bool("debug", false, "enable debug (traffic) logging")
tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
tlsKEY = flag.String("tlskey", "", "Path to TLS key PEM as seperate file")
tlsonly = flag.Bool("tlsonly", false, "Disable listening on non tls-port")
proxyTimeout = flag.Uint("proxytimeout", PROXY_TIMEOUT, "Timeout when using proxy protocol")
metrics = flag.Bool("metrics", false, "Enable metrics export")
verbose = flag.Bool("v", false, "Enable verbose logging.")
healtcheck = flag.Bool("healthcheck", false, "Enable healthcheck endpoint.")
healtbind = flag.String("healthbind", "[::]:8086", "Healthcheck bind address and port.")
permStateDir os.FileMode
permStateFile os.FileMode
permLogFile os.FileMode
clients_tls_total = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "clients_tls_connected_total",
Help: "Number of connected clients during the lifetime of the server.",
},
)
stateSink chan StateEvent = make(chan StateEvent)
clients_irc_total = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "clients_irc_connected_total",
Help: "Number of connected irc clients during the lifetime of the server.",
},
)
clients_irc_rooms_total = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "clients_irc_rooms_connected_total",
Help: "Number of clients joined to rooms during the lifetime of the server.",
},
[]string{"room"},
)
clients_connected = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "clients_connected",
Help: "Number of connected clients.",
},
)
)
func permParse(s string) os.FileMode {
@ -99,7 +133,9 @@ func listenerLoop(ln net.Listener, events chan ClientEvent) {
log.Println("error during accept", err)
continue
}
NewClient(conn, events)
client := NewClient(conn)
clients_tls_total.Inc()
go client.Processor(events)
}
}
@ -199,37 +235,76 @@ func main() {
}()
}
events := make(chan ClientEvent)
if *bind != "" {
ln, err := net.Listen("tcp", *bind)
proxyTimeout := time.Duration(uint(*proxyTimeout)) * time.Second
if *bind != "" && !*tlsonly {
listener, err := net.Listen("tcp", *bind)
if err != nil {
log.Fatalf("can not listen on %s: %v", *bind, err)
}
go listenerLoop(ln, events)
// Add PROXY-Protocol support
listener = &proxyproto.Listener{Listener: listener, ProxyHeaderTimeout: proxyTimeout}
log.Println("Raw listening on", *bind)
go listenerLoop(listener, events)
}
if *tlsBind != "" {
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM)
if *tlsKEY == "" {
tlsKEY = tlsPEM
}
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsKEY)
if err != nil {
log.Fatalf("can not load TLS keys from %s: %s", *tlsPEM, err)
log.Fatalf("Could not load Certificate and TLS keys from %s: %s", *tlsPEM, *tlsKEY, err)
}
config := tls.Config{Certificates: []tls.Certificate{cert}}
ln, err := tls.Listen("tcp", *tlsBind, &config)
listenerTLS, err := net.Listen("tcp", *tlsBind)
if err != nil {
log.Fatalf("can not listen on %s: %v", *tlsBind, err)
}
go listenerLoop(ln, events)
log.Println("TLS listening on", *tlsBind)
// Add PROXY-Protocol support
listenerTLS = &proxyproto.Listener{Listener: listenerTLS, ProxyHeaderTimeout: proxyTimeout}
listenerTLS = tls.NewListener(listenerTLS, &config)
go listenerLoop(listenerTLS, events)
}
log.Println("goircd", Version, "started")
needsShutdown := make(chan os.Signal, 0)
signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-needsShutdown
events <- ClientEvent{eventType: EventTerm}
log.Println("goircd shutting down")
}()
// Create endpoint for prometheus metrics export
if *metrics {
go prom_export()
}
if *healtcheck {
go health_endpoint()
}
finished := make(chan struct{})
go Processor(events, finished)
<-finished
Processor(events, make(chan struct{}))
}
func health_endpoint() {
health := healthchecking.NewHandler()
health.AddLivenessCheck("goroutine-threshold", healthchecking.GoroutineCountCheck(100))
log.Printf("Healthcheck listening on http://%s", *healtbind)
http.ListenAndServe(*healtbind, health)
}
func prom_export() {
prometheus.MustRegister(clients_tls_total)
prometheus.MustRegister(clients_irc_total)
prometheus.MustRegister(clients_irc_rooms_total)
prometheus.MustRegister(clients_connected)
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
}
func main() {
flag.Parse()
Run()
}

204
room.go
View File

@ -26,6 +26,16 @@ import (
"sync"
)
var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
)
// Sanitize room's name. It can consist of 1 to 50 ASCII symbols
// with some exclusions. All room names will have "#" prefix.
func RoomNameValid(name string) bool {
return RERoom.MatchString(name)
}
type Room struct {
name string
topic string
@ -37,12 +47,18 @@ type Room struct {
var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
rooms map[string]*Room = make(map[string]*Room)
roomsLock sync.RWMutex
roomsWG sync.WaitGroup
)
func (room *Room) String() (name string) {
room.RLock()
name = *room.name
room.RUnlock()
return
}
func (r *Room) SendTopic(c *Client) {
t := r.topic
if t == "" {
@ -90,23 +106,36 @@ MoreNicknames:
c.ReplyNicknamed("366", r.name, "End of NAMES list")
}
func (r *Room) Broadcast(msg string, excludes ...*Client) {
var exclude *Client
if len(excludes) > 0 {
exclude = excludes[0]
func (room *Room) Match(other string) bool {
return strings.ToLower(*room.name) == strings.ToLower(other)
}
func (room *Room) SendTopic(client *Client) {
room.RLock()
if *room.topic == "" {
client.ReplyNicknamed("331", room.String(), "No topic is set")
} else {
client.ReplyNicknamed("332", room.String(), *room.topic)
}
r.RLock()
for member := range r.members {
if member == exclude {
room.RUnlock()
}
// Send message to all room's subscribers, possibly excluding someone.
func (room *Room) Broadcast(msg string, clientToIgnore ...*Client) {
room.RLock()
for member := range room.members {
if (len(clientToIgnore) > 0) && member == clientToIgnore[0] {
continue
}
member.Msg(msg)
}
r.RUnlock()
room.RUnlock()
}
func (r *Room) StateSave() {
stateSink <- StateEvent{r.name, r.topic, r.key}
func (room *Room) StateSave() {
room.RLock()
stateSink <- StateEvent{room.String(), *room.topic, *room.key}
room.RUnlock()
}
func (r *Room) Processor(events <-chan ClientEvent) {
@ -123,46 +152,68 @@ func (r *Room) Processor(events <-chan ClientEvent) {
if *verbose {
log.Println(c, "joined", r.name)
}
r.SendTopic(c)
r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name))
logSink <- LogEvent{r.name, c.nickname, "joined", true}
r.SendNames(c)
room.Unlock()
room.SendTopic(client)
room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String()))
logSink <- LogEvent{room.String(), *client.nickname, "joined", true}
nicknames := make([]string, 0)
room.RLock()
for member := range room.members {
nicknames = append(nicknames, *member.nickname)
}
room.RUnlock()
sort.Strings(nicknames)
client.ReplyNicknamed("353", "=", room.String(), strings.Join(nicknames, " "))
client.ReplyNicknamed("366", room.String(), "End of NAMES list")
case EventDel:
if _, subscribed := r.members[c]; !subscribed {
c.ReplyNicknamed("442", r.name, "You are not on that channel")
room.RLock()
if _, subscribed := room.members[client]; !subscribed {
client.ReplyNicknamed("442", room.String(), "You are not on that channel")
room.RUnlock()
continue
}
msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname)
r.Broadcast(msg)
r.Lock()
delete(r.members, c)
r.Unlock()
logSink <- LogEvent{r.name, c.nickname, "left", true}
if *verbose {
log.Println(c, "left", r.name)
}
msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), event.text)
room.Broadcast(msg)
logSink <- LogEvent{room.String(), *client.nickname, "left", true}
room.RUnlock()
room.Lock()
delete(room.members, client)
room.Unlock()
case EventTopic:
if _, subscribed := r.members[c]; !subscribed {
c.ReplyParts("442", r.name, "You are not on that channel")
room.RLock()
if _, subscribed := room.members[client]; !subscribed {
client.ReplyParts("442", room.String(), "You are not on that channel")
room.RUnlock()
continue
}
if e.text == "" {
r.SendTopic(c)
if event.text == "" {
room.SendTopic(client)
room.RUnlock()
continue
}
topic := strings.TrimLeft(e.text, ":")
r.topic = topic
msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic)
r.Broadcast(msg)
logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true}
r.StateSave()
room.RUnlock()
topic := strings.TrimLeft(event.text, ":")
room.Lock()
room.topic = &topic
room.Unlock()
room.RLock()
msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.String(), *room.topic)
room.Broadcast(msg)
logSink <- LogEvent{
room.String(),
*client.nickname,
"set topic to " + *room.topic,
true,
}
room.RUnlock()
room.StateSave()
case EventWho:
r.RLock()
for m := range r.members {
c.ReplyNicknamed(
room.RLock()
for m := range room.members {
client.ReplyNicknamed(
"352",
r.name,
m.username,
room.String(),
*m.username,
m.Host(),
*hostname,
m.nickname,
@ -170,30 +221,36 @@ func (r *Room) Processor(events <-chan ClientEvent) {
"0 "+m.realname,
)
}
c.ReplyNicknamed("315", r.name, "End of /WHO list")
r.RUnlock()
client.ReplyNicknamed("315", room.String(), "End of /WHO list")
room.RUnlock()
case EventMode:
if e.text == "" {
mode := "+n"
if r.key != "" {
room.RLock()
if event.text == "" {
mode := "+"
if *room.key != "" {
mode = mode + "k"
}
c.Msg(fmt.Sprintf("324 %s %s %s", c.nickname, r.name, mode))
client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode))
room.RUnlock()
continue
}
if strings.HasPrefix(e.text, "b") {
c.ReplyNicknamed("368", r.name, "End of channel ban list")
if strings.HasPrefix(event.text, "b") {
client.ReplyNicknamed("368", room.String(), "End of channel ban list")
room.RUnlock()
continue
}
if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") {
if _, subscribed := r.members[c]; !subscribed {
c.ReplyParts("442", r.name, "You are not on that channel")
if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
if _, subscribed := room.members[client]; !subscribed {
client.ReplyParts("442", room.String(), "You are not on that channel")
room.RUnlock()
continue
}
} else {
c.ReplyNicknamed("472", e.text, "Unknown MODE flag")
client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
room.RUnlock()
continue
}
room.RUnlock()
var msg string
var msgLog string
if strings.HasPrefix(e.text, "+k") {
@ -202,23 +259,38 @@ func (r *Room) Processor(events <-chan ClientEvent) {
c.ReplyNotEnoughParameters("MODE")
continue
}
r.key = cols[1]
msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key)
msgLog = "set channel key"
room.Lock()
room.key = &cols[1]
msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
msgLog = "set channel key to " + *room.key
room.Unlock()
} else {
r.key = ""
msg = fmt.Sprintf(":%s MODE %s -k", c, r.name)
key := ""
room.Lock()
room.key = &key
msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
room.Unlock()
msgLog = "removed channel key"
}
r.Broadcast(msg)
logSink <- LogEvent{r.name, c.nickname, msgLog, true}
r.StateSave()
room.Broadcast(msg)
logSink <- LogEvent{room.String(), *client.nickname, msgLog, true}
room.StateSave()
case EventMsg:
sep := strings.Index(e.text, " ")
r.Broadcast(fmt.Sprintf(
":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:],
), c)
logSink <- LogEvent{r.name, c.nickname, e.text[sep+1:], false}
sep := strings.Index(event.text, " ")
room.Broadcast(fmt.Sprintf(
":%s %s %s :%s",
client,
event.text[:sep],
room.String(),
event.text[sep+1:]),
client,
)
logSink <- LogEvent{
room.String(),
*client.nickname,
event.text[sep+1:],
false,
}
}
}
}

View File

@ -46,9 +46,10 @@ func TestTwoUsers(t *testing.T) {
host := "foohost"
hostname = &host
events := make(chan ClientEvent)
roomsLock.Lock()
roomsM.Lock()
rooms = make(map[string]*Room)
roomsLock.Unlock()
roomSinks = make(map[*Room]chan ClientEvent)
roomsM.Unlock()
clients = make(map[*Client]struct{})
finished := make(chan struct{})
go Processor(events, finished)
@ -178,14 +179,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
roomsLock.RLock()
roomsM.RLock()
if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist")
}
if _, ok := rooms["#baz"]; !ok {
t.Fatal("#baz does not exist")
}
roomsLock.RUnlock()
roomsM.RUnlock()
if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("invalid join log event #bar", r)
}
@ -197,14 +198,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ {
<-conn.outbound
}
roomsLock.RLock()
if rooms["#barenc"].key != "key1" {
roomsM.RLock()
if *rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1")
}
if rooms["#bazenc"].key != "key2" {
t.Fatal("no room with key2")
}
roomsLock.RUnlock()
roomsM.RUnlock()
if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("invalid join log event #barenc", r)
}
@ -222,11 +223,11 @@ func TestJoin(t *testing.T) {
if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
t.Fatal("remove #barenc key", r)
}
roomsLock.RLock()
if rooms["#barenc"].key != "" {
roomsM.RLock()
if *rooms["#barenc"].key != "" {
t.Fatal("removing key from #barenc")
}
roomsLock.RUnlock()
roomsM.RUnlock()
if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("removed channel key log", r)
}