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

430
daemon.go
View File

@ -19,9 +19,16 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net"
"regexp"
"sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus"
) )
const ( const (
@ -31,15 +38,327 @@ const (
PingThreshold = time.Second * 90 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{}) { func Processor(events chan ClientEvent, finished chan struct{}) {
var now time.Time var now time.Time
ticker := time.NewTicker(10 * time.Second)
go func() { go func() {
for range ticker.C { for range ticker.C {
events <- ClientEvent{eventType: EventTick} events <- ClientEvent{eventType: EventTick}
} }
}() }()
EventsCycle:
for e := range events { for e := range events {
now = time.Now() now = time.Now()
client := e.client client := e.client
@ -101,10 +420,14 @@ EventsCycle:
log.Println(client, "command", cmd) log.Println(client, "command", cmd)
} }
if cmd == "QUIT" { if cmd == "QUIT" {
client.Close() log.Println(client, "quit")
if *verbose { var quitMsg string
log.Println(client, "quit") if len(cols) >= 2 {
quitMsg = strings.TrimPrefix(cols[1], ":")
} else {
quitMsg = *client.nickname
} }
client.Close(quitMsg)
continue continue
} }
if !client.registered { if !client.registered {
@ -121,14 +444,17 @@ EventsCycle:
client.ReplyNicknamed("305", "You are no longer marked as being away") client.ReplyNicknamed("305", "You are no longer marked as being away")
continue continue
} }
client.away = strings.TrimLeft(cols[1], ":") msg := cols[1]
client.away = &msg
client.ReplyNicknamed("306", "You have been marked as being away") client.ReplyNicknamed("306", "You have been marked as being away")
case "JOIN": case "JOIN":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("JOIN") client.ReplyNotEnoughParameters("JOIN")
continue continue
} }
client.Join(cols[1]) HandlerJoin(client, cols[1])
case "NICK":
ClientNick(client, cols)
case "LIST": case "LIST":
client.SendList(cols) client.SendList(cols)
case "LUSERS": case "LUSERS":
@ -139,15 +465,20 @@ EventsCycle:
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
if cols[0] == client.username { if strings.ToLower(cols[0]) == strings.ToLower(*client.username) {
client.Msg("221 " + client.nickname + " +w") if len(cols) == 1 {
// client.ReplyNicknamed("501", "Unknown MODE flag") client.Msg("221 " + *client.nickname + " +")
} else {
client.ReplyNicknamed("501", "Unknown MODE flag")
}
continue continue
} }
room := cols[0] room := cols[0]
r, found := rooms[room] roomsM.RLock()
r, found := GetRoom(room)
if !found { if !found {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
roomsM.RUnlock()
continue continue
} }
if len(cols) == 1 { if len(cols) == 1 {
@ -155,6 +486,7 @@ EventsCycle:
} else { } else {
r.events <- ClientEvent{client, EventMode, cols[1]} r.events <- ClientEvent{client, EventMode, cols[1]}
} }
roomsM.RUnlock()
case "MOTD": case "MOTD":
client.SendMotd() client.SendMotd()
case "NAMES": case "NAMES":
@ -184,16 +516,22 @@ EventsCycle:
client.ReplyNotEnoughParameters("PART") client.ReplyNotEnoughParameters("PART")
continue continue
} }
rs := strings.Split(cols[1], " ")[0] rs := strings.SplitN(cols[1], " ", 2)
roomsLock.RLock() roomsM.RLock()
for _, room := range strings.Split(rs, ",") { for _, room := range strings.Split(rs[0], ",") {
if r, found := rooms[room]; found { if r, found := GetRoom(room); found {
r.events <- ClientEvent{client, EventDel, ""} var partMsg string
if len(rs) >= 2 {
partMsg = strings.TrimPrefix(rs[1], ":")
} else {
partMsg = *client.nickname
}
roomSinks[r] <- ClientEvent{client, EventDel, partMsg}
} else { } else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
} }
} }
roomsLock.RUnlock() roomsM.RUnlock()
case "PING": case "PING":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNicknamed("409", "No origin specified") client.ReplyNicknamed("409", "No origin specified")
@ -212,10 +550,26 @@ EventsCycle:
client.ReplyNicknamed("412", "No text to send") client.ReplyNicknamed("412", "No text to send")
continue continue
} }
target := strings.ToLower(cols[0]) msg := ""
roomsLock.RLock() target := cols[0]
if r, found := rooms[target]; found { clientsM.RLock()
r.events <- ClientEvent{ 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, client,
EventMsg, EventMsg,
cmd + " " + strings.TrimLeft(cols[1], ":"), cmd + " " + strings.TrimLeft(cols[1], ":"),
@ -242,13 +596,16 @@ EventsCycle:
continue continue
} }
client.ReplyNoNickChan(target) client.ReplyNoNickChan(target)
roomsM.RUnlock()
case "TOPIC": case "TOPIC":
if len(cols) == 1 { if len(cols) == 1 {
client.ReplyNotEnoughParameters("TOPIC") client.ReplyNotEnoughParameters("TOPIC")
continue continue
} }
cols = strings.SplitN(cols[1], " ", 2) cols = strings.SplitN(cols[1], " ", 2)
r, found := rooms[cols[0]] roomsM.RLock()
r, found := GetRoom(cols[0])
roomsM.RUnlock()
if !found { if !found {
client.ReplyNoChannel(cols[0]) client.ReplyNoChannel(cols[0])
continue continue
@ -257,19 +614,22 @@ EventsCycle:
if len(cols) > 1 { if len(cols) > 1 {
change = cols[1] change = cols[1]
} }
r.events <- ClientEvent{client, EventTopic, change} roomsM.RLock()
roomSinks[r] <- ClientEvent{client, EventTopic, change}
roomsM.RUnlock()
case "WHO": case "WHO":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHO") client.ReplyNotEnoughParameters("WHO")
continue continue
} }
room := strings.Split(cols[1], " ")[0] room := strings.Split(cols[1], " ")[0]
r, found := rooms[room] roomsM.RLock()
if found { if r, found := GetRoom(room); found {
r.events <- ClientEvent{client, EventWho, ""} roomSinks[r] <- ClientEvent{client, EventWho, ""}
} else { } else {
client.ReplyNoChannel(room) client.ReplyNoChannel(room)
} }
roomsM.RUnlock()
case "WHOIS": case "WHOIS":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("WHOIS") client.ReplyNotEnoughParameters("WHOIS")
@ -277,7 +637,7 @@ EventsCycle:
} }
cols := strings.Split(cols[1], " ") cols := strings.Split(cols[1], " ")
nicknames := strings.Split(cols[len(cols)-1], ",") nicknames := strings.Split(cols[len(cols)-1], ",")
client.SendWhois(nicknames) SendWhois(client, nicknames)
case "ISON": case "ISON":
if len(cols) == 1 || len(cols[1]) < 1 { if len(cols) == 1 || len(cols[1]) < 1 {
client.ReplyNotEnoughParameters("ISON") client.ReplyNotEnoughParameters("ISON")
@ -288,13 +648,13 @@ EventsCycle:
for _, nickname := range nicknamesList { for _, nickname := range nicknamesList {
nicknames[nickname] = false nicknames[nickname] = false
} }
clientsLock.RLock() clientsM.RLock()
for c := range clients { for c := range clients {
if _, exists := nicknames[c.nickname]; exists { if _, exists := nicknames[c.nickname]; exists {
nicknames[c.nickname] = true nicknames[c.nickname] = true
} }
} }
clientsLock.RUnlock() clientsM.RUnlock()
nicknamesList = nicknamesList[:0] nicknamesList = nicknamesList[:0]
for n, exists := range nicknames { for n, exists := range nicknames {
if exists { if exists {
@ -318,6 +678,17 @@ EventsCycle:
for _, c := range cs { for _, c := range cs {
c.Msg(fmt.Sprintf(":%s NOTICE %s %s", client, c.nickname, cols[1])) 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": case "VERSION":
var debug string var debug string
if *verbose { if *verbose {
@ -345,6 +716,7 @@ EventsCycle:
// Read their EventDel // Read their EventDel
go func() { go func() {
for range events { 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

159
goircd.go
View File

@ -19,17 +19,27 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"flag"
"io/ioutil" "io/ioutil"
"log" "log"
"net" "net"
"net/http"
"os" "os"
"os/signal"
"path" "path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "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 ( const (
@ -61,27 +71,51 @@ type StateEvent struct {
} }
var ( var (
hostname = flag.String("hostname", "localhost", "hostname") version string
bind = flag.String("bind", "[::1]:6667", "address to bind to") hostname = flag.String("hostname", "localhost", "Hostname")
cloak = flag.String("cloak", "", "cloak user's host with the given hostname") bind = flag.String("bind", ":6667", "Address to bind to")
motd = flag.String("motd", "", "path to MOTD file") motd = flag.String("motd", "", "Path to MOTD file")
logdir = flag.String("logdir", "", "absolute path to directory for logs") logdir = flag.String("logdir", "", "Absolute path to directory for logs")
statedir = flag.String("statedir", "", "absolute path to directory for states") statedir = flag.String("statedir", "", "Absolute path to directory for states")
passwords = flag.String("passwd", "", "optional path to passwords file") passwords = flag.String("passwords", "", "Optional path to passwords file")
tlsBind = flag.String("tlsbind", "", "TLS address to bind to") tlsBind = flag.String("tlsbind", "", "TLS address to bind to")
tlsPEM = flag.String("tlspem", "", "path to TLS certificat+key PEM file") tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file")
permStateDirS = flag.String("perm-state-dir", "755", "state directory permissions") tlsKEY = flag.String("tlskey", "", "Path to TLS key PEM as seperate file")
permStateFileS = flag.String("perm-state-file", "600", "state files permissions") tlsonly = flag.Bool("tlsonly", false, "Disable listening on non tls-port")
permLogFileS = flag.String("perm-log-file", "644", "log files permissions") proxyTimeout = flag.Uint("proxytimeout", PROXY_TIMEOUT, "Timeout when using proxy protocol")
timestamped = flag.Bool("timestamped", false, "enable timestamps on stderr messages") metrics = flag.Bool("metrics", false, "Enable metrics export")
verbose = flag.Bool("verbose", false, "enable verbose logging") verbose = flag.Bool("v", false, "Enable verbose logging.")
debug = flag.Bool("debug", false, "enable debug (traffic) logging") healtcheck = flag.Bool("healthcheck", false, "Enable healthcheck endpoint.")
healtbind = flag.String("healthbind", "[::]:8086", "Healthcheck bind address and port.")
permStateDir os.FileMode clients_tls_total = prometheus.NewCounter(
permStateFile os.FileMode prometheus.CounterOpts{
permLogFile os.FileMode 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 { func permParse(s string) os.FileMode {
@ -99,7 +133,9 @@ func listenerLoop(ln net.Listener, events chan ClientEvent) {
log.Println("error during accept", err) log.Println("error during accept", err)
continue 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) proxyTimeout := time.Duration(uint(*proxyTimeout)) * time.Second
if *bind != "" {
ln, err := net.Listen("tcp", *bind) if *bind != "" && !*tlsonly {
listener, err := net.Listen("tcp", *bind)
if err != nil { if err != nil {
log.Fatalf("can not listen on %s: %v", *bind, err) 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 != "" { if *tlsBind != "" {
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM) if *tlsKEY == "" {
tlsKEY = tlsPEM
}
cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsKEY)
if err != nil { 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}} config := tls.Config{Certificates: []tls.Certificate{cert}}
ln, err := tls.Listen("tcp", *tlsBind, &config)
listenerTLS, err := net.Listen("tcp", *tlsBind)
if err != nil { if err != nil {
log.Fatalf("can not listen on %s: %v", *tlsBind, err) 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) // Create endpoint for prometheus metrics export
signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT) if *metrics {
go func() { go prom_export()
<-needsShutdown }
events <- ClientEvent{eventType: EventTerm} if *healtcheck {
log.Println("goircd shutting down") go health_endpoint()
}() }
finished := make(chan struct{}) Processor(events, make(chan struct{}))
go Processor(events, finished) }
<-finished
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()
} }

206
room.go
View File

@ -26,6 +26,16 @@ import (
"sync" "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 { type Room struct {
name string name string
topic string topic string
@ -36,13 +46,19 @@ type Room struct {
} }
var ( var (
RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$") RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$")
rooms map[string]*Room = make(map[string]*Room) rooms map[string]*Room = make(map[string]*Room)
roomsLock sync.RWMutex roomsLock sync.RWMutex
roomsWG sync.WaitGroup roomsWG sync.WaitGroup
) )
func (room *Room) String() (name string) {
room.RLock()
name = *room.name
room.RUnlock()
return
}
func (r *Room) SendTopic(c *Client) { func (r *Room) SendTopic(c *Client) {
t := r.topic t := r.topic
if t == "" { if t == "" {
@ -90,23 +106,36 @@ MoreNicknames:
c.ReplyNicknamed("366", r.name, "End of NAMES list") c.ReplyNicknamed("366", r.name, "End of NAMES list")
} }
func (r *Room) Broadcast(msg string, excludes ...*Client) { func (room *Room) Match(other string) bool {
var exclude *Client return strings.ToLower(*room.name) == strings.ToLower(other)
if len(excludes) > 0 { }
exclude = excludes[0]
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() room.RUnlock()
for member := range r.members { }
if member == exclude {
// 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 continue
} }
member.Msg(msg) member.Msg(msg)
} }
r.RUnlock() room.RUnlock()
} }
func (r *Room) StateSave() { func (room *Room) StateSave() {
stateSink <- StateEvent{r.name, r.topic, r.key} room.RLock()
stateSink <- StateEvent{room.String(), *room.topic, *room.key}
room.RUnlock()
} }
func (r *Room) Processor(events <-chan ClientEvent) { func (r *Room) Processor(events <-chan ClientEvent) {
@ -123,46 +152,68 @@ func (r *Room) Processor(events <-chan ClientEvent) {
if *verbose { if *verbose {
log.Println(c, "joined", r.name) log.Println(c, "joined", r.name)
} }
r.SendTopic(c) room.Unlock()
r.Broadcast(fmt.Sprintf(":%s JOIN %s", c, r.name)) room.SendTopic(client)
logSink <- LogEvent{r.name, c.nickname, "joined", true} room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, room.String()))
r.SendNames(c) 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: case EventDel:
if _, subscribed := r.members[c]; !subscribed { room.RLock()
c.ReplyNicknamed("442", r.name, "You are not on that channel") if _, subscribed := room.members[client]; !subscribed {
client.ReplyNicknamed("442", room.String(), "You are not on that channel")
room.RUnlock()
continue continue
} }
msg := fmt.Sprintf(":%s PART %s :%s", c, r.name, c.nickname) msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), event.text)
r.Broadcast(msg) room.Broadcast(msg)
r.Lock() logSink <- LogEvent{room.String(), *client.nickname, "left", true}
delete(r.members, c) room.RUnlock()
r.Unlock() room.Lock()
logSink <- LogEvent{r.name, c.nickname, "left", true} delete(room.members, client)
if *verbose { room.Unlock()
log.Println(c, "left", r.name)
}
case EventTopic: case EventTopic:
if _, subscribed := r.members[c]; !subscribed { room.RLock()
c.ReplyParts("442", r.name, "You are not on that channel") if _, subscribed := room.members[client]; !subscribed {
client.ReplyParts("442", room.String(), "You are not on that channel")
room.RUnlock()
continue continue
} }
if e.text == "" { if event.text == "" {
r.SendTopic(c) room.SendTopic(client)
room.RUnlock()
continue continue
} }
topic := strings.TrimLeft(e.text, ":") room.RUnlock()
r.topic = topic topic := strings.TrimLeft(event.text, ":")
msg := fmt.Sprintf(":%s TOPIC %s :%s", c, r.name, r.topic) room.Lock()
r.Broadcast(msg) room.topic = &topic
logSink <- LogEvent{r.name, c.nickname, "set topic to " + r.topic, true} room.Unlock()
r.StateSave() 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: case EventWho:
r.RLock() room.RLock()
for m := range r.members { for m := range room.members {
c.ReplyNicknamed( client.ReplyNicknamed(
"352", "352",
r.name, room.String(),
m.username, *m.username,
m.Host(), m.Host(),
*hostname, *hostname,
m.nickname, m.nickname,
@ -170,30 +221,36 @@ func (r *Room) Processor(events <-chan ClientEvent) {
"0 "+m.realname, "0 "+m.realname,
) )
} }
c.ReplyNicknamed("315", r.name, "End of /WHO list") client.ReplyNicknamed("315", room.String(), "End of /WHO list")
r.RUnlock() room.RUnlock()
case EventMode: case EventMode:
if e.text == "" { room.RLock()
mode := "+n" if event.text == "" {
if r.key != "" { mode := "+"
if *room.key != "" {
mode = mode + "k" 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 continue
} }
if strings.HasPrefix(e.text, "b") { if strings.HasPrefix(event.text, "b") {
c.ReplyNicknamed("368", r.name, "End of channel ban list") client.ReplyNicknamed("368", room.String(), "End of channel ban list")
room.RUnlock()
continue continue
} }
if strings.HasPrefix(e.text, "-k") || strings.HasPrefix(e.text, "+k") { if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") {
if _, subscribed := r.members[c]; !subscribed { if _, subscribed := room.members[client]; !subscribed {
c.ReplyParts("442", r.name, "You are not on that channel") client.ReplyParts("442", room.String(), "You are not on that channel")
room.RUnlock()
continue continue
} }
} else { } else {
c.ReplyNicknamed("472", e.text, "Unknown MODE flag") client.ReplyNicknamed("472", event.text, "Unknown MODE flag")
room.RUnlock()
continue continue
} }
room.RUnlock()
var msg string var msg string
var msgLog string var msgLog string
if strings.HasPrefix(e.text, "+k") { if strings.HasPrefix(e.text, "+k") {
@ -202,23 +259,38 @@ func (r *Room) Processor(events <-chan ClientEvent) {
c.ReplyNotEnoughParameters("MODE") c.ReplyNotEnoughParameters("MODE")
continue continue
} }
r.key = cols[1] room.Lock()
msg = fmt.Sprintf(":%s MODE %s +k %s", c, r.name, r.key) room.key = &cols[1]
msgLog = "set channel key" msg = fmt.Sprintf(":%s MODE %s +k %s", client, *room.name, *room.key)
msgLog = "set channel key to " + *room.key
room.Unlock()
} else { } else {
r.key = "" key := ""
msg = fmt.Sprintf(":%s MODE %s -k", c, r.name) room.Lock()
room.key = &key
msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name)
room.Unlock()
msgLog = "removed channel key" msgLog = "removed channel key"
} }
r.Broadcast(msg) room.Broadcast(msg)
logSink <- LogEvent{r.name, c.nickname, msgLog, true} logSink <- LogEvent{room.String(), *client.nickname, msgLog, true}
r.StateSave() room.StateSave()
case EventMsg: case EventMsg:
sep := strings.Index(e.text, " ") sep := strings.Index(event.text, " ")
r.Broadcast(fmt.Sprintf( room.Broadcast(fmt.Sprintf(
":%s %s %s :%s", c, e.text[:sep], r.name, e.text[sep+1:], ":%s %s %s :%s",
), c) client,
logSink <- LogEvent{r.name, c.nickname, e.text[sep+1:], false} 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" host := "foohost"
hostname = &host hostname = &host
events := make(chan ClientEvent) events := make(chan ClientEvent)
roomsLock.Lock() roomsM.Lock()
rooms = make(map[string]*Room) rooms = make(map[string]*Room)
roomsLock.Unlock() roomSinks = make(map[*Room]chan ClientEvent)
roomsM.Unlock()
clients = make(map[*Client]struct{}) clients = make(map[*Client]struct{})
finished := make(chan struct{}) finished := make(chan struct{})
go Processor(events, finished) go Processor(events, finished)
@ -178,14 +179,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
roomsLock.RLock() roomsM.RLock()
if _, ok := rooms["#bar"]; !ok { if _, ok := rooms["#bar"]; !ok {
t.Fatal("#bar does not exist") t.Fatal("#bar does not exist")
} }
if _, ok := rooms["#baz"]; !ok { if _, ok := rooms["#baz"]; !ok {
t.Fatal("#baz does not exist") 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) { if r := <-logSink; (r.what != "joined") || (r.where != "#bar") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("invalid join log event #bar", r) t.Fatal("invalid join log event #bar", r)
} }
@ -197,14 +198,14 @@ func TestJoin(t *testing.T) {
for i := 0; i < 4*2; i++ { for i := 0; i < 4*2; i++ {
<-conn.outbound <-conn.outbound
} }
roomsLock.RLock() roomsM.RLock()
if rooms["#barenc"].key != "key1" { if *rooms["#barenc"].key != "key1" {
t.Fatal("no room with key1") t.Fatal("no room with key1")
} }
if rooms["#bazenc"].key != "key2" { if rooms["#bazenc"].key != "key2" {
t.Fatal("no room with 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) { if r := <-logSink; (r.what != "joined") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("invalid join log event #barenc", r) 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" { if r := <-conn.outbound; r != ":nick2!foo2@someclient MODE #barenc -k\r\n" {
t.Fatal("remove #barenc key", r) t.Fatal("remove #barenc key", r)
} }
roomsLock.RLock() roomsM.RLock()
if rooms["#barenc"].key != "" { if *rooms["#barenc"].key != "" {
t.Fatal("removing key from #barenc") 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) { if r := <-logSink; (r.what != "removed channel key") || (r.where != "#barenc") || (r.who != "nick2") || (r.meta != true) {
t.Fatal("removed channel key log", r) t.Fatal("removed channel key log", r)
} }