diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4549da4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/* +!.git/ +!*.go +!GNUmakefile +!common.mk +!Dockerfile +!.dockerignore \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index c3b08ed..be67a40 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ * Sergey Matveev * Thomas Habets -* Björn Busse +* Björn Busse +* steigr diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9977511 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..91a9baa --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,4 @@ +VERSION = $(shell git describe --tags) +PACKAGE ?= quay.io/goircd/goircd + +include common.mk diff --git a/charts/goircd/.helmignore b/charts/goircd/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/charts/goircd/.helmignore @@ -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 diff --git a/charts/goircd/Chart.yaml b/charts/goircd/Chart.yaml new file mode 100644 index 0000000..0f23601 --- /dev/null +++ b/charts/goircd/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: minimalistic simple Internet Relay Chat (IRC) server +name: goircd +version: 0.1.0 diff --git a/charts/goircd/templates/NOTES.txt b/charts/goircd/templates/NOTES.txt new file mode 100644 index 0000000..777e140 --- /dev/null +++ b/charts/goircd/templates/NOTES.txt @@ -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 }} diff --git a/charts/goircd/templates/_helpers.tpl b/charts/goircd/templates/_helpers.tpl new file mode 100644 index 0000000..0802273 --- /dev/null +++ b/charts/goircd/templates/_helpers.tpl @@ -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 -}} diff --git a/charts/goircd/templates/configmap.yaml b/charts/goircd/templates/configmap.yaml new file mode 100644 index 0000000..f27edf1 --- /dev/null +++ b/charts/goircd/templates/configmap.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/goircd/templates/deployment.yaml b/charts/goircd/templates/deployment.yaml new file mode 100644 index 0000000..2ea3ace --- /dev/null +++ b/charts/goircd/templates/deployment.yaml @@ -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 }} diff --git a/charts/goircd/templates/service.yaml b/charts/goircd/templates/service.yaml new file mode 100644 index 0000000..2446307 --- /dev/null +++ b/charts/goircd/templates/service.yaml @@ -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 }} diff --git a/charts/goircd/values.yaml b/charts/goircd/values.yaml new file mode 100644 index 0000000..bd03db2 --- /dev/null +++ b/charts/goircd/values.yaml @@ -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: {} diff --git a/client.go b/client.go index 0bfc537..257f2c8 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,10 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server +<<<<<<< HEAD Copyright (C) 2014-2021 Sergey Matveev +======= +Copyright (C) 2014-2016 Sergey Matveev +>>>>>>> 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) { diff --git a/common.mk b/common.mk new file mode 100644 index 0000000..ed5b175 --- /dev/null +++ b/common.mk @@ -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 diff --git a/daemon.go b/daemon.go index b7aee88..6fb17e1 100644 --- a/daemon.go +++ b/daemon.go @@ -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") + 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)) } }() diff --git a/events.go b/events.go new file mode 100644 index 0000000..10bb31c --- /dev/null +++ b/events.go @@ -0,0 +1,120 @@ +/* +goircd -- minimalistic simple Internet Relay Chat (IRC) server +Copyright (C) 2014-2016 Sergey Matveev + +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 . +*/ + +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) + } + } +} diff --git a/examples/proxy-protocol/haproxy.cfg b/examples/proxy-protocol/haproxy.cfg new file mode 100644 index 0000000..2f6bfdf --- /dev/null +++ b/examples/proxy-protocol/haproxy.cfg @@ -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 diff --git a/examples/proxy-protocol/haproxy.sh b/examples/proxy-protocol/haproxy.sh new file mode 100755 index 0000000..9428968 --- /dev/null +++ b/examples/proxy-protocol/haproxy.sh @@ -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 \ No newline at end of file diff --git a/goircd.go b/goircd.go index 524b66d..79e0082 100644 --- a/goircd.go +++ b/goircd.go @@ -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") - 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") + 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") + 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() } diff --git a/room.go b/room.go index 3b52a1b..fcf0e3d 100644 --- a/room.go +++ b/room.go @@ -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 @@ -36,13 +46,19 @@ type Room struct { } 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) 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, + } } } } diff --git a/room_test.go b/room_test.go index 8638cc4..13b0f86 100644 --- a/room_test.go +++ b/room_test.go @@ -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) }