Merge remote-tracking branch 'github/master'
This commit is contained in:
commit
b7a5c66929
|
@ -0,0 +1,7 @@
|
||||||
|
**/*
|
||||||
|
!.git/
|
||||||
|
!*.go
|
||||||
|
!GNUmakefile
|
||||||
|
!common.mk
|
||||||
|
!Dockerfile
|
||||||
|
!.dockerignore
|
3
AUTHORS
3
AUTHORS
|
@ -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>
|
||||||
|
|
|
@ -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"]
|
|
@ -0,0 +1,4 @@
|
||||||
|
VERSION = $(shell git describe --tags)
|
||||||
|
PACKAGE ?= quay.io/goircd/goircd
|
||||||
|
|
||||||
|
include common.mk
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
apiVersion: v1
|
||||||
|
appVersion: "1.0"
|
||||||
|
description: minimalistic simple Internet Relay Chat (IRC) server
|
||||||
|
name: goircd
|
||||||
|
version: 0.1.0
|
|
@ -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 }}
|
|
@ -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 -}}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -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: {}
|
40
client.go
40
client.go
|
@ -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) {
|
||||||
|
|
|
@ -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
430
daemon.go
|
@ -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))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
159
goircd.go
|
@ -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
206
room.go
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
room_test.go
21
room_test.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue