From b4e0bbf0a46ea685437e2ba4a3d7b6f40f7e5a94 Mon Sep 17 00:00:00 2001 From: steigr Date: Tue, 20 Feb 2018 19:48:13 +0100 Subject: [PATCH 01/34] Wrap up net.Conn with go-proxyproto.Conn Wrap up connection into proxy-protocol handler. See examples/proxy-protocol/haproxy* to test it Signed-off-by: steigr --- client.go | 7 ++-- examples/proxy-protocol/haproxy.cfg | 13 +++++++ examples/proxy-protocol/haproxy.sh | 54 +++++++++++++++++++++++++++++ goircd.go | 10 +++++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 examples/proxy-protocol/haproxy.cfg create mode 100755 examples/proxy-protocol/haproxy.sh diff --git a/client.go b/client.go index a43cc1e..3d6d9c3 100644 --- a/client.go +++ b/client.go @@ -21,10 +21,11 @@ package main import ( "bytes" "log" - "net" "strings" "sync" "time" + + proxyproto "github.com/Freeaqingme/go-proxyproto" ) const ( @@ -37,7 +38,7 @@ var ( ) type Client struct { - conn net.Conn + conn *proxyproto.Conn registered bool nickname *string username *string @@ -55,7 +56,7 @@ func (c Client) String() string { return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String() } -func NewClient(conn net.Conn) *Client { +func NewClient(conn *proxyproto.Conn) *Client { nickname := "*" username := "" c := Client{ 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..c279f0e --- /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() { echjo "$*"; 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 8bc5e3e..2cced26 100644 --- a/goircd.go +++ b/goircd.go @@ -27,6 +27,9 @@ import ( "path" "path/filepath" "strings" + "time" + + proxyproto "github.com/Freeaqingme/go-proxyproto" ) var ( @@ -42,6 +45,10 @@ var ( verbose = flag.Bool("v", false, "Enable verbose logging.") ) +const ( + PROXY_TIMEOUT = 5 * time.Second +) + func listenerLoop(sock net.Listener, events chan ClientEvent) { for { conn, err := sock.Accept() @@ -49,7 +56,8 @@ func listenerLoop(sock net.Listener, events chan ClientEvent) { log.Println("Error during accepting connection", err) continue } - client := NewClient(conn) + proxied_conn := proxyproto.NewConn(conn, PROXY_TIMEOUT) + client := NewClient(proxied_conn) go client.Processor(events) } } From 398bedcca440d1fc75a915bf052be106026779be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Sun, 4 Mar 2018 19:50:32 +0000 Subject: [PATCH 02/34] Do not trim leading colon from msg --- daemon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 57797c7..3d1c8ea 100644 --- a/daemon.go +++ b/daemon.go @@ -344,7 +344,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { client.ReplyNicknamed("305", "You are no longer marked as being away") continue } - msg := strings.TrimLeft(cols[1], ":") + msg := cols[1] client.away = &msg client.ReplyNicknamed("306", "You have been marked as being away") case "JOIN": From 0b56bd0ba45de64059329cbcfd9ac599c6aaa0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Sun, 18 Oct 2015 22:07:57 +0200 Subject: [PATCH 03/34] Perform DNS lookup of client addresses --- README | 2 +- client.go | 13 ++++++++++++- room.go | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README b/README index b6291ea..3793978 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ It does not aim to replace full featured mass scalable IRC networks: * It can not connect to other servers. Just standalone installation * It has few basic IRC commands * There is no support for channel operators, modes, votes, invites -* No ident lookups, reverse DNS queries +* No ident lookups But it has some convincing features: diff --git a/client.go b/client.go index 3d6d9c3..8438dab 100644 --- a/client.go +++ b/client.go @@ -52,8 +52,19 @@ type Client struct { sync.Mutex } +func (c Client) Host() string { + addr := c.conn.RemoteAddr().String() + if host, _, err := net.SplitHostPort(addr); err == nil { + addr = host + } + if domains, err := net.LookupAddr(addr); err == nil { + addr = strings.TrimSuffix(domains[0], ".") + } + return addr +} + func (c Client) String() string { - return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String() + return *c.nickname + "!" + *c.username + "@" + c.Host() } func NewClient(conn *proxyproto.Conn) *Client { diff --git a/room.go b/room.go index 2a5f22a..276397f 100644 --- a/room.go +++ b/room.go @@ -142,7 +142,7 @@ func (room *Room) Processor(events <-chan ClientEvent) { "352", *room.name, *m.username, - m.conn.RemoteAddr().String(), + m.Host(), *hostname, *m.nickname, "H", From 69b0c4617048102bb0c8e0aa65358918bc521bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Sun, 18 Oct 2015 23:47:50 +0200 Subject: [PATCH 04/34] Do not alter MOTD It should be up to the author how exactly the MOTD is laid out - some people like to have a margin, therefor remove the trim(). --- daemon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 3d1c8ea..e56b7b5 100644 --- a/daemon.go +++ b/daemon.go @@ -67,7 +67,7 @@ func SendMotd(client *Client) { return } client.ReplyNicknamed("375", "- "+*hostname+" Message of the day -") - for _, s := range strings.Split(strings.Trim(string(motdText), "\n"), "\n") { + for _, s := range strings.Split(strings.TrimSuffix(string(motdText), "\n"), "\n") { client.ReplyNicknamed("372", "- "+s) } client.ReplyNicknamed("376", "End of /MOTD command") From cd4faa7dd69ddd983884eeac2ada893f342b11d2 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Mon, 19 Oct 2015 22:16:47 +0300 Subject: [PATCH 05/34] =?UTF-8?q?Thanks=20to=20Bj=C3=B6rn=20for=20his=20pa?= =?UTF-8?q?tches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 1efa17f..c3b08ed 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ * Sergey Matveev * Thomas Habets +* Björn Busse From 493dc78a24eb9451f245720db40b70df5556a46d Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 4 Nov 2015 17:37:58 +0300 Subject: [PATCH 06/34] Simplify channel closing ordering --- client.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/client.go b/client.go index 8438dab..859365c 100644 --- a/client.go +++ b/client.go @@ -47,7 +47,7 @@ type Client struct { away *string recvTimestamp time.Time sendTimestamp time.Time - outBuf chan string + outBuf chan *string alive bool sync.Mutex } @@ -77,20 +77,19 @@ func NewClient(conn *proxyproto.Conn) *Client { recvTimestamp: time.Now(), sendTimestamp: time.Now(), alive: true, - outBuf: make(chan string, MaxOutBuf), + outBuf: make(chan *string, MaxOutBuf), } go c.MsgSender() return &c } func (c *Client) SetDead() { - close(c.outBuf) + c.outBuf <- nil c.alive = false } func (c *Client) Close() { c.Lock() - c.conn.Close() if c.alive { c.SetDead() } @@ -134,7 +133,11 @@ func (c *Client) Processor(sink chan ClientEvent) { func (c *Client) MsgSender() { for msg := range c.outBuf { - c.conn.Write(append([]byte(msg), CRLF...)) + if msg == nil { + c.conn.Close() + break + } + c.conn.Write(append([]byte(*msg), CRLF...)) } } @@ -147,11 +150,12 @@ func (c *Client) Msg(text string) { } if len(c.outBuf) == MaxOutBuf { log.Println(c, "output buffer size exceeded, kicking him") - go c.Close() - c.SetDead() + if c.alive { + c.SetDead() + } return } - c.outBuf <- text + c.outBuf <- &text } // Send message from server. It has ": servername" prefix. From d5f10048e1ad15a8b3d3526dace3c05ef71d0d3f Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 4 Nov 2015 17:53:20 +0300 Subject: [PATCH 07/34] If no states are kept, then clear an empty rooms --- daemon.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/daemon.go b/daemon.go index e56b7b5..238b7c8 100644 --- a/daemon.go +++ b/daemon.go @@ -308,6 +308,14 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } } } + for rn, r := range rooms { + if *statedir == "" && len(r.members) == 0 { + log.Println(rn, "emptied room") + delete(rooms, rn) + close(roomSinks[r]) + delete(roomSinks, r) + } + } case EventTerm: for _, sink := range roomSinks { sink <- ClientEvent{eventType: EventTerm} From e354be75954c1671aa53048ed0b4e7c153fdca19 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 4 Nov 2015 17:56:44 +0300 Subject: [PATCH 08/34] Trivial formatting correction --- README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 3793978..0a11809 100644 --- a/README +++ b/README @@ -52,8 +52,8 @@ Just execute goircd daemon. It has following optional arguments: -statedir: directory where all channels states will be saved and loaded during startup. If omitted, then states will be lost after daemon termination - -tlsbind : enable TLS, specify address to listen on and path - -tlspem to PEM file with certificate and private key + -tlsbind: enable TLS, specify address to listen on and path + -tlspem to PEM file with certificate and private key -passwords: enable client authentication and specify path to passwords file -v: increase verbosity From 5413b3ae52e4f1c1d1ec7e875dd6e2ce74a3b998 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 5 Nov 2015 17:04:52 +0300 Subject: [PATCH 09/34] Force lowercase nicknames --- daemon.go | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon.go b/daemon.go index 238b7c8..c01dbde 100644 --- a/daemon.go +++ b/daemon.go @@ -162,6 +162,7 @@ func ClientRegister(client *Client, cmd string, cols []string) { nickname := cols[1] // Compatibility with some clients prepending colons to nickname nickname = strings.TrimPrefix(nickname, ":") + nickname = strings.ToLower(nickname) for existingClient := range clients { if *existingClient.nickname == nickname { client.ReplyParts("433", "*", nickname, "Nickname is already in use") From 61e337b083cb8f3c4bd17c72f79b23cb5ff01739 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 14 Nov 2015 18:54:34 +0300 Subject: [PATCH 10/34] ISON command support --- README | 4 ++-- daemon.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README b/README index 0a11809..f3a0ec0 100644 --- a/README +++ b/README @@ -4,7 +4,7 @@ DESCRIPTION goircd is very simple IRC server, written on Go. -It is heavily inspired by miniircd daemon written on Python. +It was heavily inspired by miniircd daemon written on Python. GoVPN is free software: see the file COPYING for copying conditions. It does not aim to replace full featured mass scalable IRC networks: @@ -35,7 +35,7 @@ SUPPORTED IRC COMMANDS * PASS/NICK/USER during registration workflow * PING/PONGs -* NOTICE/PRIVMSG +* NOTICE/PRIVMSG, ISON * AWAY, MOTD, LUSERS, WHO, WHOIS, VERSION, QUIT * LIST, JOIN, TOPIC, +k/-k channel MODE diff --git a/daemon.go b/daemon.go index c01dbde..be5de5c 100644 --- a/daemon.go +++ b/daemon.go @@ -486,6 +486,22 @@ func Processor(events chan ClientEvent, finished chan struct{}) { cols := strings.Split(cols[1], " ") nicknames := strings.Split(cols[len(cols)-1], ",") SendWhois(client, nicknames) + case "ISON": + if len(cols) == 1 || len(cols[1]) < 1 { + client.ReplyNotEnoughParameters("ISON") + continue + } + nicksKnown := make(map[string]struct{}) + for c := range clients { + nicksKnown[*c.nickname] = struct{}{} + } + var nicksExists []string + for _, nickname := range strings.Split(cols[1], " ") { + if _, exists := nicksKnown[nickname]; exists { + nicksExists = append(nicksExists, nickname) + } + } + client.ReplyNicknamed("303", strings.Join(nicksExists, " ")) case "VERSION": var debug string if *verbose { From 8fd97af456760315208694969f9c497a86b52ec8 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 10 Dec 2015 11:27:14 +0300 Subject: [PATCH 11/34] Fix receive timestamp setting --- daemon.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon.go b/daemon.go index be5de5c..c1ce5a9 100644 --- a/daemon.go +++ b/daemon.go @@ -346,6 +346,9 @@ func Processor(events chan ClientEvent, finished chan struct{}) { ClientRegister(client, cmd, cols) continue } + if client != nil { + client.recvTimestamp = now + } switch cmd { case "AWAY": if len(cols) == 1 { @@ -514,8 +517,5 @@ func Processor(events chan ClientEvent, finished chan struct{}) { client.ReplyNicknamed("421", cmd, "Unknown command") } } - if client != nil { - client.recvTimestamp = now - } } } From 5ca748d9283c6663b8a9f26747ae03a20b757628 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 31 Dec 2015 20:23:21 +0300 Subject: [PATCH 12/34] Raise copyright years --- client.go | 2 +- client_test.go | 2 +- common_test.go | 2 +- daemon.go | 2 +- daemon_test.go | 2 +- events.go | 2 +- goircd.go | 2 +- room.go | 2 +- room_test.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client.go b/client.go index 859365c..2e61d83 100644 --- a/client.go +++ b/client.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/client_test.go b/client_test.go index 4c9682e..be05d71 100644 --- a/client_test.go +++ b/client_test.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/common_test.go b/common_test.go index 9c4cb05..2b2836b 100644 --- a/common_test.go +++ b/common_test.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/daemon.go b/daemon.go index c1ce5a9..8d61a99 100644 --- a/daemon.go +++ b/daemon.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/daemon_test.go b/daemon_test.go index e48d22d..08ac480 100644 --- a/daemon_test.go +++ b/daemon_test.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/events.go b/events.go index 9f3673b..10bb31c 100644 --- a/events.go +++ b/events.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/goircd.go b/goircd.go index 2cced26..05105a9 100644 --- a/goircd.go +++ b/goircd.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/room.go b/room.go index 276397f..dcad9d4 100644 --- a/room.go +++ b/room.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 diff --git a/room_test.go b/room_test.go index 8d6decc..58f4158 100644 --- a/room_test.go +++ b/room_test.go @@ -1,6 +1,6 @@ /* goircd -- minimalistic simple Internet Relay Chat (IRC) server -Copyright (C) 2014-2015 Sergey Matveev +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 From 51766fc90dd4846b175fae56e79ecfba3d5957ba Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 26 Mar 2016 17:10:04 +0300 Subject: [PATCH 13/34] Various race preventing locks --- client.go | 4 +-- daemon.go | 56 ++++++++++++++++++++++++++++++-- room.go | 92 +++++++++++++++++++++++++++++++++++++--------------- room_test.go | 10 +++++- 4 files changed, 130 insertions(+), 32 deletions(-) diff --git a/client.go b/client.go index 2e61d83..0b7549f 100644 --- a/client.go +++ b/client.go @@ -52,7 +52,7 @@ type Client struct { sync.Mutex } -func (c Client) Host() string { +func (c *Client) Host() string { addr := c.conn.RemoteAddr().String() if host, _, err := net.SplitHostPort(addr); err == nil { addr = host @@ -63,7 +63,7 @@ func (c Client) Host() string { return addr } -func (c Client) String() string { +func (c *Client) String() string { return *c.nickname + "!" + *c.username + "@" + c.Host() } diff --git a/daemon.go b/daemon.go index 8d61a99..21bda0d 100644 --- a/daemon.go +++ b/daemon.go @@ -40,18 +40,23 @@ const ( var ( RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$") + 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 - - clients map[*Client]struct{} = make(map[*Client]struct{}) + roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent) ) func SendLusers(client *Client) { lusers := 0 + clientsM.RLock() for client := range clients { if client.registered { lusers++ } } + clientsM.RUnlock() client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers)) } @@ -82,11 +87,14 @@ func SendWhois(client *Client, nicknames []string) { var subscriber *Client for _, nickname := range nicknames { nickname = strings.ToLower(nickname) + clientsM.RLock() for c = range clients { if strings.ToLower(*c.nickname) == nickname { + clientsM.RUnlock() goto Found } } + clientsM.RUnlock() client.ReplyNoNickChan(nickname) continue Found: @@ -101,6 +109,7 @@ func SendWhois(client *Client, nicknames []string) { client.ReplyNicknamed("301", *c.nickname, *c.away) } subscriptions = make([]string, 0) + roomsM.RLock() for _, room = range rooms { for subscriber = range room.members { if *subscriber.nickname == nickname { @@ -108,6 +117,7 @@ func SendWhois(client *Client, nicknames []string) { } } } + roomsM.RUnlock() sort.Strings(subscriptions) client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " ")) client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list") @@ -121,14 +131,17 @@ func SendList(client *Client, cols []string) { 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", @@ -137,6 +150,7 @@ func SendList(client *Client, cols []string) { *room.topic, ) } + roomsM.RUnlock() } client.ReplyNicknamed("323", "End of /LIST") } @@ -163,12 +177,15 @@ func ClientRegister(client *Client, cmd string, cols []string) { // Compatibility with some clients prepending colons to nickname nickname = strings.TrimPrefix(nickname, ":") nickname = strings.ToLower(nickname) + clientsM.RLock() for existingClient := range clients { if *existingClient.nickname == 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 @@ -227,8 +244,10 @@ func ClientRegister(client *Client, cmd string, cols []string) { func RoomRegister(name string) (*Room, chan ClientEvent) { roomNew := NewRoom(name) roomSink := make(chan ClientEvent) + roomsM.Lock() rooms[name] = roomNew roomSinks[roomNew] = roomSink + roomsM.Unlock() go roomNew.Processor(roomSink) roomsGroup.Add(1) return roomNew, roomSink @@ -257,8 +276,10 @@ func HandlerJoin(client *Client, cmd string) { } else { key = "" } + roomsM.RLock() for roomExisting, roomSink = range roomSinks { if room == *roomExisting.name { + roomsM.RUnlock() if (*roomExisting.key != "") && (*roomExisting.key != key) { goto Denied } @@ -266,6 +287,7 @@ func HandlerJoin(client *Client, cmd string) { goto Joined } } + roomsM.RUnlock() roomNew, roomSink = RoomRegister(room) log.Println("Room", roomNew, "created") if key != "" { @@ -293,6 +315,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { client := event.client switch event.eventType { case EventTick: + clientsM.RLock() for c := range clients { if c.recvTimestamp.Add(PingTimeout).Before(now) { log.Println(c, "ping timeout") @@ -309,6 +332,8 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } } } + clientsM.RUnlock() + roomsM.Lock() for rn, r := range rooms { if *statedir == "" && len(r.members) == 0 { log.Println(rn, "emptied room") @@ -317,20 +342,29 @@ func Processor(events chan ClientEvent, finished chan struct{}) { delete(roomSinks, r) } } + roomsM.Unlock() case EventTerm: + roomsM.RLock() for _, sink := range roomSinks { sink <- ClientEvent{eventType: EventTerm} } + roomsM.RUnlock() roomsGroup.Wait() close(finished) return case EventNew: + clientsM.Lock() clients[client] = struct{}{} + clientsM.Unlock() case EventDel: + clientsM.Lock() delete(clients, client) + clientsM.Unlock() + roomsM.RLock() for _, roomSink := range roomSinks { roomSink <- event } + roomsM.RUnlock() case EventMsg: cols := strings.SplitN(event.text, " ", 2) cmd := strings.ToUpper(cols[0]) @@ -384,9 +418,11 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } room := cols[0] + roomsM.RLock() r, found := rooms[room] if !found { client.ReplyNoChannel(room) + roomsM.RUnlock() continue } if len(cols) == 1 { @@ -394,6 +430,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } else { roomSinks[r] <- ClientEvent{client, EventMode, cols[1]} } + roomsM.RUnlock() case "MOTD": SendMotd(client) case "PART": @@ -402,14 +439,17 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } rs := strings.Split(cols[1], " ")[0] + roomsM.RLock() for _, room := range strings.Split(rs, ",") { if r, found := rooms[room]; found { roomSinks[r] <- ClientEvent{client, EventDel, ""} } else { + roomsM.RUnlock() client.ReplyNoChannel(room) continue } } + roomsM.RUnlock() case "PING": if len(cols) == 1 { client.ReplyNicknamed("409", "No origin specified") @@ -430,6 +470,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } msg := "" target := strings.ToLower(cols[0]) + clientsM.RLock() for c := range clients { if *c.nickname == target { msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1]) @@ -440,9 +481,11 @@ func Processor(events chan ClientEvent, finished chan struct{}) { break } } + clientsM.RUnlock() if msg != "" { continue } + roomsM.RLock() if r, found := rooms[target]; found { roomSinks[r] <- ClientEvent{ client, @@ -452,13 +495,16 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } else { client.ReplyNoNickChan(target) } + roomsM.RUnlock() case "TOPIC": if len(cols) == 1 { client.ReplyNotEnoughParameters("TOPIC") continue } cols = strings.SplitN(cols[1], " ", 2) + roomsM.RLock() r, found := rooms[cols[0]] + roomsM.RUnlock() if !found { client.ReplyNoChannel(cols[0]) continue @@ -469,18 +515,22 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } else { 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] + roomsM.RLock() if r, found := rooms[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") @@ -495,9 +545,11 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } nicksKnown := make(map[string]struct{}) + clientsM.RLock() for c := range clients { nicksKnown[*c.nickname] = struct{}{} } + clientsM.RUnlock() var nicksExists []string for _, nickname := range strings.Split(cols[1], " ") { if _, exists := nicksKnown[nickname]; exists { diff --git a/room.go b/room.go index dcad9d4..d5693e8 100644 --- a/room.go +++ b/room.go @@ -24,14 +24,11 @@ import ( "regexp" "sort" "strings" + "sync" ) var ( RERoom = regexp.MustCompile("^#[^\x00\x07\x0a\x0d ,:/]{1,200}$") - - rooms map[string]*Room = make(map[string]*Room) - - roomSinks map[*Room]chan ClientEvent = make(map[*Room]chan ClientEvent) ) // Sanitize room's name. It can consist of 1 to 50 ASCII symbols @@ -45,10 +42,14 @@ type Room struct { topic *string key *string members map[*Client]struct{} + sync.RWMutex } -func (room Room) String() string { - return *room.name +func (room *Room) String() (name string) { + room.RLock() + name = *room.name + room.RUnlock() + return } func NewRoom(name string) *Room { @@ -63,25 +64,31 @@ func NewRoom(name string) *Room { } func (room *Room) SendTopic(client *Client) { + room.RLock() if *room.topic == "" { - client.ReplyNicknamed("331", *room.name, "No topic is set") + client.ReplyNicknamed("331", room.String(), "No topic is set") } else { - client.ReplyNicknamed("332", *room.name, *room.topic) + client.ReplyNicknamed("332", room.String(), *room.topic) } + 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) } + room.RUnlock() } func (room *Room) StateSave() { - stateSink <- StateEvent{*room.name, *room.topic, *room.key} + room.RLock() + stateSink <- StateEvent{room.String(), *room.topic, *room.key} + room.RUnlock() } func (room *Room) Processor(events <-chan ClientEvent) { @@ -93,54 +100,74 @@ func (room *Room) Processor(events <-chan ClientEvent) { roomsGroup.Done() return case EventNew: + room.Lock() room.members[client] = struct{}{} if *verbose { log.Println(client, "joined", room.name) } + room.Unlock() room.SendTopic(client) - room.Broadcast(fmt.Sprintf(":%s JOIN %s", client, *room.name)) - logSink <- LogEvent{*room.name, *client.nickname, "joined", true} + 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.name, strings.Join(nicknames, " ")) - client.ReplyNicknamed("366", *room.name, "End of NAMES list") + client.ReplyNicknamed("353", "=", room.String(), strings.Join(nicknames, " ")) + client.ReplyNicknamed("366", room.String(), "End of NAMES list") case EventDel: + room.RLock() if _, subscribed := room.members[client]; !subscribed { - client.ReplyNicknamed("442", *room.name, "You are not on that channel") + client.ReplyNicknamed("442", room.String(), "You are not on that channel") + room.RUnlock() continue } + room.RUnlock() + room.Lock() delete(room.members, client) - msg := fmt.Sprintf(":%s PART %s :%s", client, *room.name, *client.nickname) + room.Unlock() + room.RLock() + msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname) room.Broadcast(msg) - logSink <- LogEvent{*room.name, *client.nickname, "left", true} + logSink <- LogEvent{room.String(), *client.nickname, "left", true} + room.RUnlock() case EventTopic: + room.RLock() if _, subscribed := room.members[client]; !subscribed { - client.ReplyParts("442", *room.name, "You are not on that channel") + client.ReplyParts("442", room.String(), "You are not on that channel") + room.RUnlock() continue } if event.text == "" { room.SendTopic(client) + room.RUnlock() continue } + room.RUnlock() topic := strings.TrimLeft(event.text, ":") + room.Lock() room.topic = &topic - msg := fmt.Sprintf(":%s TOPIC %s :%s", client, *room.name, *room.topic) + room.Unlock() + room.RLock() + msg := fmt.Sprintf(":%s TOPIC %s :%s", client, room.String(), *room.topic) room.Broadcast(msg) logSink <- LogEvent{ - *room.name, + room.String(), *client.nickname, "set topic to " + *room.topic, true, } + room.RUnlock() room.StateSave() case EventWho: + room.RLock() for m := range room.members { client.ReplyNicknamed( "352", - *room.name, + room.String(), *m.username, m.Host(), *hostname, @@ -149,29 +176,36 @@ func (room *Room) Processor(events <-chan ClientEvent) { "0 "+*m.realname, ) } - client.ReplyNicknamed("315", *room.name, "End of /WHO list") + client.ReplyNicknamed("315", room.String(), "End of /WHO list") + room.RUnlock() case EventMode: + room.RLock() if event.text == "" { mode := "+" if *room.key != "" { mode = mode + "k" } - client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, *room.name, mode)) + client.Msg(fmt.Sprintf("324 %s %s %s", *client.nickname, room.String(), mode)) + room.RUnlock() continue } if strings.HasPrefix(event.text, "b") { - client.ReplyNicknamed("368", *room.name, "End of channel ban list") + client.ReplyNicknamed("368", room.String(), "End of channel ban list") + room.RUnlock() continue } if strings.HasPrefix(event.text, "-k") || strings.HasPrefix(event.text, "+k") { if _, subscribed := room.members[client]; !subscribed { - client.ReplyParts("442", *room.name, "You are not on that channel") + client.ReplyParts("442", room.String(), "You are not on that channel") + room.RUnlock() continue } } else { client.ReplyNicknamed("472", event.text, "Unknown MODE flag") + room.RUnlock() continue } + room.RUnlock() var msg string var msgLog string if strings.HasPrefix(event.text, "+k") { @@ -180,17 +214,21 @@ func (room *Room) Processor(events <-chan ClientEvent) { client.ReplyNotEnoughParameters("MODE") continue } + 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 { key := "" + room.Lock() room.key = &key msg = fmt.Sprintf(":%s MODE %s -k", client, *room.name) + room.Unlock() msgLog = "removed channel key" } room.Broadcast(msg) - logSink <- LogEvent{*room.name, *client.nickname, msgLog, true} + logSink <- LogEvent{room.String(), *client.nickname, msgLog, true} room.StateSave() case EventMsg: sep := strings.Index(event.text, " ") @@ -198,12 +236,12 @@ func (room *Room) Processor(events <-chan ClientEvent) { ":%s %s %s :%s", client, event.text[:sep], - *room.name, + room.String(), event.text[sep+1:]), client, ) logSink <- LogEvent{ - *room.name, + room.String(), *client.nickname, event.text[sep+1:], false, diff --git a/room_test.go b/room_test.go index 58f4158..8618fb9 100644 --- a/room_test.go +++ b/room_test.go @@ -47,9 +47,11 @@ func TestTwoUsers(t *testing.T) { host := "foohost" hostname = &host events := make(chan ClientEvent) + roomsM.Lock() rooms = make(map[string]*Room) - clients = make(map[*Client]struct{}) roomSinks = make(map[*Room]chan ClientEvent) + roomsM.Unlock() + clients = make(map[*Client]struct{}) finished := make(chan struct{}) go Processor(events, finished) defer func() { @@ -182,12 +184,14 @@ func TestJoin(t *testing.T) { for i := 0; i < 4*2; i++ { <-conn.outbound } + roomsM.RLock() if _, ok := rooms["#bar"]; !ok { t.Fatal("#bar does not exist") } if _, ok := rooms["#baz"]; !ok { t.Fatal("#baz does not exist") } + 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) } @@ -199,12 +203,14 @@ func TestJoin(t *testing.T) { for i := 0; i < 4*2; i++ { <-conn.outbound } + roomsM.RLock() if *rooms["#barenc"].key != "key1" { t.Fatal("no room with key1") } if *rooms["#bazenc"].key != "key2" { t.Fatal("no room with key2") } + 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,9 +228,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) } + roomsM.RLock() if *rooms["#barenc"].key != "" { t.Fatal("removing key from #barenc") } + 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) } From 1dbc0856bd18805c0d79e15a587b427d982e8663 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 26 Mar 2016 17:29:15 +0300 Subject: [PATCH 14/34] Example lighttpd configuration for logs directory viewing --- lighttpd.conf | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lighttpd.conf diff --git a/lighttpd.conf b/lighttpd.conf new file mode 100644 index 0000000..684c3c2 --- /dev/null +++ b/lighttpd.conf @@ -0,0 +1,12 @@ +mimetype.assign = ( + "" => "text/plain" +) + +server.document-root = "/home/goircd/logs" +server.errorlog = "lighttpd.err" +server.pid-file = "lighttpd.pid" +server.port = 80 + +dir-listing.activate = "enable" +dir-listing.encoding = "UTF-8" +dir-listing.show-readme = "enable" From 2d71da2234823fc7cb182961ec869f493a83e08a Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Thu, 7 Apr 2016 18:33:27 +0300 Subject: [PATCH 15/34] Trim possible :-prefix sent by Pidgin in PASS command --- daemon.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 21bda0d..a7ac2c6 100644 --- a/daemon.go +++ b/daemon.go @@ -167,7 +167,8 @@ func ClientRegister(client *Client, cmd string, cols []string) { client.ReplyNotEnoughParameters("PASS") return } - client.password = &cols[1] + password := strings.TrimPrefix(cols[1], ":") + client.password = &password case "NICK": if len(cols) == 1 || len(cols[1]) < 1 { client.ReplyParts("431", "No nickname given") From a9a5b17ce45ca110e7b24f2533ac796869497576 Mon Sep 17 00:00:00 2001 From: Gunnar Ruthenberg Date: Sun, 4 Mar 2018 17:20:25 +0000 Subject: [PATCH 16/34] Fixes by Gunnar --- client.go | 14 +++-- daemon.go | 159 +++++++++++++++++++++++++++++++++++------------------- room.go | 12 +++-- 3 files changed, 122 insertions(+), 63 deletions(-) diff --git a/client.go b/client.go index 0b7549f..88a9fa7 100644 --- a/client.go +++ b/client.go @@ -49,6 +49,7 @@ type Client struct { sendTimestamp time.Time outBuf chan *string alive bool + quitMsg *string sync.Mutex } @@ -67,13 +68,19 @@ func (c *Client) String() string { return *c.nickname + "!" + *c.username + "@" + c.Host() } +func (c *Client) Match(other string) bool { + return strings.ToLower(*c.nickname) == strings.ToLower(other) +} + func NewClient(conn *proxyproto.Conn) *Client { nickname := "*" username := "" + realname := "" c := Client{ conn: conn, nickname: &nickname, username: &username, + realname: &realname, recvTimestamp: time.Now(), sendTimestamp: time.Now(), alive: true, @@ -88,9 +95,10 @@ func (c *Client) SetDead() { c.alive = false } -func (c *Client) Close() { +func (c *Client) Close(text string) { c.Lock() if c.alive { + c.quitMsg = &text c.SetDead() } c.Unlock() @@ -127,8 +135,8 @@ func (c *Client) Processor(sink chan ClientEvent) { prev -= (i + 2) goto CheckMore } - c.Close() - sink <- ClientEvent{c, EventDel, ""} + c.Close("error") + sink <- ClientEvent{c, EventDel, *c.quitMsg} } func (c *Client) MsgSender() { diff --git a/daemon.go b/daemon.go index a7ac2c6..4b29257 100644 --- a/daemon.go +++ b/daemon.go @@ -38,7 +38,7 @@ const ( ) var ( - RENickname = regexp.MustCompile("^[a-zA-Z0-9-]{1,24}$") + 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 @@ -48,6 +48,18 @@ var ( 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 SendLusers(client *Client) { lusers := 0 clientsM.RLock() @@ -86,11 +98,9 @@ func SendWhois(client *Client, nicknames []string) { var room *Room var subscriber *Client for _, nickname := range nicknames { - nickname = strings.ToLower(nickname) clientsM.RLock() for c = range clients { - if strings.ToLower(*c.nickname) == nickname { - clientsM.RUnlock() + if c.Match(nickname) { goto Found } } @@ -112,7 +122,7 @@ func SendWhois(client *Client, nicknames []string) { roomsM.RLock() for _, room = range rooms { for subscriber = range room.members { - if *subscriber.nickname == nickname { + if subscriber.Match(nickname) { subscriptions = append(subscriptions, *room.name) } } @@ -121,6 +131,7 @@ func SendWhois(client *Client, nicknames []string) { sort.Strings(subscriptions) client.ReplyNicknamed("319", *c.nickname, strings.Join(subscriptions, " ")) client.ReplyNicknamed("318", *c.nickname, "End of /WHOIS list") + clientsM.RUnlock() } } @@ -145,7 +156,7 @@ func SendList(client *Client, cols []string) { if room, found = rooms[r]; found { client.ReplyNicknamed( "322", - r, + *room.name, fmt.Sprintf("%d", len(room.members)), *room.topic, ) @@ -155,6 +166,54 @@ func SendList(client *Client, cols []string) { 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 @@ -170,28 +229,7 @@ func ClientRegister(client *Client, cmd string, cols []string) { password := strings.TrimPrefix(cols[1], ":") client.password = &password case "NICK": - 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, ":") - nickname = strings.ToLower(nickname) - clientsM.RLock() - for existingClient := range clients { - if *existingClient.nickname == 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 - } - client.nickname = &nickname + ClientNick(client, cols) case "USER": if len(cols) == 1 { client.ReplyNotEnoughParameters("USER") @@ -210,7 +248,7 @@ func ClientRegister(client *Client, cmd string, cols []string) { if passwords != nil && *passwords != "" { if client.password == nil { client.ReplyParts("462", "You may not register") - client.Close() + client.Close("462") return } contents, err := ioutil.ReadFile(*passwords) @@ -224,7 +262,7 @@ func ClientRegister(client *Client, cmd string, cols []string) { } if lp := strings.Split(entry, ":"); lp[0] == *client.nickname && lp[1] != *client.password { client.ReplyParts("462", "You may not register") - client.Close() + client.Close("462") return } } @@ -246,7 +284,7 @@ func RoomRegister(name string) (*Room, chan ClientEvent) { roomNew := NewRoom(name) roomSink := make(chan ClientEvent) roomsM.Lock() - rooms[name] = roomNew + rooms[strings.ToLower(name)] = roomNew roomSinks[roomNew] = roomSink roomsM.Unlock() go roomNew.Processor(roomSink) @@ -279,7 +317,7 @@ func HandlerJoin(client *Client, cmd string) { } roomsM.RLock() for roomExisting, roomSink = range roomSinks { - if room == *roomExisting.name { + if roomExisting.Match(room) { roomsM.RUnlock() if (*roomExisting.key != "") && (*roomExisting.key != key) { goto Denied @@ -320,7 +358,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { for c := range clients { if c.recvTimestamp.Add(PingTimeout).Before(now) { log.Println(c, "ping timeout") - c.Close() + c.Close("ping timeout") continue } if c.sendTimestamp.Add(PingThreshold).Before(now) { @@ -329,7 +367,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { c.sendTimestamp = time.Now() } else { log.Println(c, "ping timeout") - c.Close() + c.Close("ping timeout") } } } @@ -374,7 +412,13 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } if cmd == "QUIT" { log.Println(client, "quit") - client.Close() + var quitMsg string + if len(cols) >= 2 { + quitMsg = strings.TrimPrefix(cols[1], ":") + } else { + quitMsg = *client.nickname + } + client.Close(quitMsg) continue } if !client.registered { @@ -400,6 +444,8 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } HandlerJoin(client, cols[1]) + case "NICK": + ClientNick(client, cols) case "LIST": SendList(client, cols) case "LUSERS": @@ -410,7 +456,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } cols = strings.SplitN(cols[1], " ", 2) - if cols[0] == *client.username { + if strings.ToLower(cols[0]) == strings.ToLower(*client.username) { if len(cols) == 1 { client.Msg("221 " + *client.nickname + " +") } else { @@ -420,7 +466,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } room := cols[0] roomsM.RLock() - r, found := rooms[room] + r, found := GetRoom(room) if !found { client.ReplyNoChannel(room) roomsM.RUnlock() @@ -439,13 +485,18 @@ func Processor(events chan ClientEvent, finished chan struct{}) { client.ReplyNotEnoughParameters("PART") continue } - rs := strings.Split(cols[1], " ")[0] + rs := strings.SplitN(cols[1], " ", 2) roomsM.RLock() - for _, room := range strings.Split(rs, ",") { - if r, found := rooms[room]; found { - roomSinks[r] <- ClientEvent{client, EventDel, ""} + 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 { - roomsM.RUnlock() client.ReplyNoChannel(room) continue } @@ -470,10 +521,10 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } msg := "" - target := strings.ToLower(cols[0]) + target := cols[0] clientsM.RLock() for c := range clients { - if *c.nickname == target { + if c.Match(target) { msg = fmt.Sprintf(":%s %s %s %s", client, cmd, *c.nickname, cols[1]) c.Msg(msg) if c.away != nil { @@ -487,7 +538,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } roomsM.RLock() - if r, found := rooms[target]; found { + if r, found := rooms[strings.ToLower(target)]; found { roomSinks[r] <- ClientEvent{ client, EventMsg, @@ -504,7 +555,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { } cols = strings.SplitN(cols[1], " ", 2) roomsM.RLock() - r, found := rooms[cols[0]] + r, found := GetRoom(cols[0]) roomsM.RUnlock() if !found { client.ReplyNoChannel(cols[0]) @@ -525,8 +576,8 @@ func Processor(events chan ClientEvent, finished chan struct{}) { continue } room := strings.Split(cols[1], " ")[0] - roomsM.RLock() - if r, found := rooms[room]; found { + roomsM.RLock() + if r, found := GetRoom(room); found { roomSinks[r] <- ClientEvent{client, EventWho, ""} } else { client.ReplyNoChannel(room) @@ -545,18 +596,16 @@ func Processor(events chan ClientEvent, finished chan struct{}) { client.ReplyNotEnoughParameters("ISON") continue } - nicksKnown := make(map[string]struct{}) clientsM.RLock() - for c := range clients { - nicksKnown[*c.nickname] = struct{}{} - } - clientsM.RUnlock() var nicksExists []string for _, nickname := range strings.Split(cols[1], " ") { - if _, exists := nicksKnown[nickname]; exists { - nicksExists = append(nicksExists, nickname) + 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 diff --git a/room.go b/room.go index d5693e8..29945e3 100644 --- a/room.go +++ b/room.go @@ -63,6 +63,10 @@ func NewRoom(name string) *Room { } } +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 == "" { @@ -125,15 +129,13 @@ func (room *Room) Processor(events <-chan ClientEvent) { room.RUnlock() continue } + 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() - room.RLock() - msg := fmt.Sprintf(":%s PART %s :%s", client, room.String(), *client.nickname) - room.Broadcast(msg) - logSink <- LogEvent{room.String(), *client.nickname, "left", true} - room.RUnlock() case EventTopic: room.RLock() if _, subscribed := room.members[client]; !subscribed { From eb84358fefb66be8d65a35ae91d96db42eda2441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Sun, 4 Mar 2018 22:53:24 +0000 Subject: [PATCH 17/34] Re-add net import --- client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client.go b/client.go index 88a9fa7..22c7ce5 100644 --- a/client.go +++ b/client.go @@ -21,6 +21,7 @@ package main import ( "bytes" "log" + "net" "strings" "sync" "time" From 30151254c26d0bf0e6771871ca599bfa5e4c78b1 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Tue, 6 Mar 2018 22:52:43 +0100 Subject: [PATCH 18/34] Fix typo Signed-off-by: Mathias Kaufmann --- examples/proxy-protocol/haproxy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/proxy-protocol/haproxy.sh b/examples/proxy-protocol/haproxy.sh index c279f0e..9428968 100755 --- a/examples/proxy-protocol/haproxy.sh +++ b/examples/proxy-protocol/haproxy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash [[ -f examples/proxy-protocol/haproxy.pid ]] && rm examples/proxy-protocol/haproxy.pid -fail() { echjo "$*"; exit 1; } +fail() { echo "$*"; exit 1; } set -x which haproxy || fail haproxy is missing From b0536016a1eb5b7fda18b2072f300dd239751b6e Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Tue, 6 Mar 2018 22:53:04 +0100 Subject: [PATCH 19/34] Fix handling of TLS-Connections Signed-off-by: Mathias Kaufmann --- client.go | 7 +++---- goircd.go | 46 ++++++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/client.go b/client.go index 3d6d9c3..a43cc1e 100644 --- a/client.go +++ b/client.go @@ -21,11 +21,10 @@ package main import ( "bytes" "log" + "net" "strings" "sync" "time" - - proxyproto "github.com/Freeaqingme/go-proxyproto" ) const ( @@ -38,7 +37,7 @@ var ( ) type Client struct { - conn *proxyproto.Conn + conn net.Conn registered bool nickname *string username *string @@ -56,7 +55,7 @@ func (c Client) String() string { return *c.nickname + "!" + *c.username + "@" + c.conn.RemoteAddr().String() } -func NewClient(conn *proxyproto.Conn) *Client { +func NewClient(conn net.Conn) *Client { nickname := "*" username := "" c := Client{ diff --git a/goircd.go b/goircd.go index 2cced26..4471183 100644 --- a/goircd.go +++ b/goircd.go @@ -32,21 +32,22 @@ import ( proxyproto "github.com/Freeaqingme/go-proxyproto" ) -var ( - 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") - verbose = flag.Bool("v", false, "Enable verbose logging.") +const ( + PROXY_TIMEOUT = 5 ) -const ( - PROXY_TIMEOUT = 5 * time.Second +var ( + 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") + proxyTimeout = flag.Uint("proxytimeout", PROXY_TIMEOUT, "Timeout when using proxy protocol") + verbose = flag.Bool("v", false, "Enable verbose logging.") ) func listenerLoop(sock net.Listener, events chan ClientEvent) { @@ -56,8 +57,7 @@ func listenerLoop(sock net.Listener, events chan ClientEvent) { log.Println("Error during accepting connection", err) continue } - proxied_conn := proxyproto.NewConn(conn, PROXY_TIMEOUT) - client := NewClient(proxied_conn) + client := NewClient(conn) go client.Processor(events) } } @@ -114,25 +114,39 @@ func Run() { log.Println(*statedir, "statekeeper initialized") } + proxyTimeout := time.Duration(uint(*proxyTimeout)) * time.Second + if *bind != "" { listener, err := net.Listen("tcp", *bind) if err != nil { log.Fatalf("Can not listen on %s: %v", *bind, err) } + // 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 err != nil { log.Fatalf("Could not load TLS keys from %s: %s", *tlsPEM, err) } config := tls.Config{Certificates: []tls.Certificate{cert}} - listenerTLS, 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) } 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) } Processor(events, make(chan struct{})) From 9a4e42ba8b2d56c47be8a5857f87728d71d7b29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Thu, 8 Mar 2018 02:40:26 +0000 Subject: [PATCH 20/34] Add a prometheus exporter and some metrics --- daemon.go | 21 ++++++++++++++++++--- goircd.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/daemon.go b/daemon.go index 4b29257..97e9854 100644 --- a/daemon.go +++ b/daemon.go @@ -28,6 +28,8 @@ import ( "strings" "sync" "time" + + "github.com/prometheus/client_golang/prometheus" ) const ( @@ -60,15 +62,20 @@ func GetRoom(name string) (r *Room, found bool) { return r, found } -func SendLusers(client *Client) { - lusers := 0 +func GetNumberOfRegisteredUsers(client *Client) (nusers float64){ + nusers = 0 clientsM.RLock() for client := range clients { if client.registered { - lusers++ + nusers++ } } clientsM.RUnlock() + return nusers +} + +func SendLusers(client *Client) { + lusers := GetNumberOfRegisteredUsers(client) client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers)) } @@ -268,6 +275,8 @@ func ClientRegister(client *Client, cmd string, cols []string) { } } 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") @@ -338,17 +347,22 @@ func HandlerJoin(client *Client, cmd string) { 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 + go func() { for { time.Sleep(10 * time.Second) events <- ClientEvent{eventType: EventTick} } }() + + for event := range events { now = time.Now() client := event.client @@ -618,6 +632,7 @@ func Processor(events chan ClientEvent, finished chan struct{}) { default: client.ReplyNicknamed("421", cmd, "Unknown command") } + clients_connected.Set(GetNumberOfRegisteredUsers(client)) } } } diff --git a/goircd.go b/goircd.go index fa32bb1..d827d0c 100644 --- a/goircd.go +++ b/goircd.go @@ -24,12 +24,15 @@ import ( "io/ioutil" "log" "net" + "net/http" "path" "path/filepath" "strings" "time" proxyproto "github.com/Freeaqingme/go-proxyproto" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) const ( @@ -47,7 +50,37 @@ var ( tlsBind = flag.String("tlsbind", "", "TLS address to bind to") tlsPEM = flag.String("tlspem", "", "Path to TLS certificat+key PEM file") 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.") + + clients_tls_total = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "clients_tls_connected_total", + Help: "Number of connected clients during the lifetime of the server.", + }, + ) + + 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 listenerLoop(sock net.Listener, events chan ClientEvent) { @@ -58,6 +91,7 @@ func listenerLoop(sock net.Listener, events chan ClientEvent) { continue } client := NewClient(conn) + clients_tls_total.Inc() go client.Processor(events) } } @@ -149,9 +183,25 @@ func Run() { go listenerLoop(listenerTLS, events) } + + // Create endpoint for prometheus metrics export + if *metrics { + go prom_export() + } + Processor(events, make(chan struct{})) } +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() From de42ef98b6f726aa09bd6a5da0528cd2c21ebf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Thu, 8 Mar 2018 17:11:58 +0000 Subject: [PATCH 21/34] Fix type of lusers --- daemon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.go b/daemon.go index 97e9854..f01cc76 100644 --- a/daemon.go +++ b/daemon.go @@ -75,7 +75,7 @@ func GetNumberOfRegisteredUsers(client *Client) (nusers float64){ } func SendLusers(client *Client) { - lusers := GetNumberOfRegisteredUsers(client) + lusers := int(GetNumberOfRegisteredUsers(client)) client.ReplyNicknamed("251", fmt.Sprintf("There are %d users and 0 invisible on 1 servers", lusers)) } From dec20c2cd0b04a1d91ae7b479b6086f282bd168b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Thu, 8 Mar 2018 17:37:26 +0000 Subject: [PATCH 22/34] Add a tlsonly option --- goircd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/goircd.go b/goircd.go index d827d0c..a48b33e 100644 --- a/goircd.go +++ b/goircd.go @@ -49,6 +49,7 @@ var ( 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") + 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.") @@ -150,7 +151,7 @@ func Run() { proxyTimeout := time.Duration(uint(*proxyTimeout)) * time.Second - if *bind != "" { + if *bind != "" && !*tlsonly { listener, err := net.Listen("tcp", *bind) if err != nil { log.Fatalf("Can not listen on %s: %v", *bind, err) From 06d17fe26b09821c66ccb22f785db60331a7bc2a Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Tue, 13 Mar 2018 23:36:53 +0100 Subject: [PATCH 23/34] use github.com/namsral/flag for flags github.com/namsral/flag enables environment and config file parsing Signed-off-by: Mathias Kaufmann --- goircd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/goircd.go b/goircd.go index a48b33e..128dcf3 100644 --- a/goircd.go +++ b/goircd.go @@ -20,7 +20,6 @@ package main import ( "crypto/tls" - "flag" "io/ioutil" "log" "net" @@ -30,6 +29,8 @@ import ( "strings" "time" + "github.com/namsral/flag" + proxyproto "github.com/Freeaqingme/go-proxyproto" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" From 36e84da7b5e8a242df1e46e5c7e5fdbf20384728 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Tue, 13 Mar 2018 23:52:03 +0100 Subject: [PATCH 24/34] Added Dockerfile Signed-off-by: Mathias Kaufmann --- .dockerignore | 7 +++++++ Dockerfile | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1bb5e5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.10 AS goircd-builder +ARG PACKAGE=github.com/bbusse/goircd +ENV PACKAGE=$PACKAGE + +WORKDIR /go/src/github.com/bbusse/goircd/ + +ADD . /go/src/github.com/bbusse/goircd/ + +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 ["sh","-c"] +CMD ["exec goircd"] \ No newline at end of file From eeb6782f0f441b3634ebe3dfc585baea2864ac7f Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 00:10:00 +0100 Subject: [PATCH 25/34] Added make targets to build and push docker images Usage: `make docker-image` to build the image `make docker-image-push` to build and push the image Signed-off-by: Mathias Kaufmann --- Dockerfile | 4 ++-- GNUmakefile | 3 ++- common.mk | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1bb5e5b..9708ce4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,9 @@ FROM golang:1.10 AS goircd-builder ARG PACKAGE=github.com/bbusse/goircd ENV PACKAGE=$PACKAGE -WORKDIR /go/src/github.com/bbusse/goircd/ +WORKDIR /go/src/$PACKAGE/ -ADD . /go/src/github.com/bbusse/goircd/ +ADD . /go/src/$PACKAGE/ RUN export CGO_ENABLED=0 \ && go get $PACKAGE \ diff --git a/GNUmakefile b/GNUmakefile index 04d452d..c7aeb93 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,3 +1,4 @@ VERSION = $(shell git describe --tags) +PACKAGE ?= quay.io/goircd/goircd -include common.mk +include common.mk \ No newline at end of file diff --git a/common.mk b/common.mk index 888e048..ed5b175 100644 --- a/common.mk +++ b/common.mk @@ -2,3 +2,17 @@ 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 From c8e83bb840f3f4972f9dd1aaea13b2a6d17f6191 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 00:11:01 +0100 Subject: [PATCH 26/34] Added trailing newline Signed-off-by: Mathias Kaufmann --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index c7aeb93..91a9baa 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,4 +1,4 @@ VERSION = $(shell git describe --tags) PACKAGE ?= quay.io/goircd/goircd -include common.mk \ No newline at end of file +include common.mk From 154aaa524397e024373d4f3ec2b0972532d0e9b6 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 01:05:57 +0100 Subject: [PATCH 27/34] Added healthchecking via HTTP Signed-off-by: Mathias Kaufmann --- goircd.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/goircd.go b/goircd.go index 128dcf3..e30db55 100644 --- a/goircd.go +++ b/goircd.go @@ -29,6 +29,7 @@ import ( "strings" "time" + healthchecking "github.com/heptiolabs/healthcheck" "github.com/namsral/flag" proxyproto "github.com/Freeaqingme/go-proxyproto" @@ -37,7 +38,8 @@ import ( ) const ( - PROXY_TIMEOUT = 5 + PROXY_TIMEOUT = 5 + HEALTCHECK_PORT = 8080 ) var ( @@ -54,6 +56,7 @@ var ( 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.") clients_tls_total = prometheus.NewCounter( prometheus.CounterOpts{ @@ -190,10 +193,23 @@ func Run() { if *metrics { go prom_export() } + if *healtcheck { + go health_endpoint() + } Processor(events, make(chan struct{})) } +func health_endpoint() { + var ( + health_bind = "0.0.0.0:8086" + ) + health := healthchecking.NewHandler() + health.AddLivenessCheck("goroutine-threshold", healthchecking.GoroutineCountCheck(100)) + log.Printf("Healthcheck listening on http://%s", health_bind) + http.ListenAndServe(health_bind, health) +} + func prom_export() { prometheus.MustRegister(clients_tls_total) prometheus.MustRegister(clients_irc_total) From 2678bcb0626b7bf103f5a0e5b7dc2ee9589fcc58 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 01:08:43 +0100 Subject: [PATCH 28/34] Fixed entrypoint Signed-off-by: Mathias Kaufmann --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9708ce4..9977511 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,5 +13,4 @@ RUN export CGO_ENABLED=0 \ FROM alpine AS goircd COPY --from=goircd-builder /go/bin/goircd /bin/goircd -ENTRYPOINT ["sh","-c"] -CMD ["exec goircd"] \ No newline at end of file +ENTRYPOINT ["goircd"] From 95f4035392e2c781abccf65ce13dee0059cc6041 Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 01:09:17 +0100 Subject: [PATCH 29/34] Added helm chart Signed-off-by: Mathias Kaufmann --- charts/goircd/.helmignore | 21 ++++++++ charts/goircd/Chart.yaml | 5 ++ charts/goircd/templates/NOTES.txt | 15 ++++++ charts/goircd/templates/_helpers.tpl | 32 ++++++++++++ charts/goircd/templates/configmap.yaml | 11 ++++ charts/goircd/templates/deployment.yaml | 67 +++++++++++++++++++++++++ charts/goircd/templates/service.yaml | 19 +++++++ charts/goircd/values.yaml | 37 ++++++++++++++ 8 files changed, 207 insertions(+) create mode 100644 charts/goircd/.helmignore create mode 100644 charts/goircd/Chart.yaml create mode 100644 charts/goircd/templates/NOTES.txt create mode 100644 charts/goircd/templates/_helpers.tpl create mode 100644 charts/goircd/templates/configmap.yaml create mode 100644 charts/goircd/templates/deployment.yaml create mode 100644 charts/goircd/templates/service.yaml create mode 100644 charts/goircd/values.yaml 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: {} From 6e35f78338d86fae70e807eb83a8db7400d8ee5b Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Wed, 14 Mar 2018 02:06:18 +0100 Subject: [PATCH 30/34] Allow to specify tlsKEY as seperate file. Signed-off-by: Mathias Kaufmann --- goircd.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/goircd.go b/goircd.go index 128dcf3..79f8a13 100644 --- a/goircd.go +++ b/goircd.go @@ -50,6 +50,7 @@ var ( 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") @@ -165,9 +166,14 @@ func Run() { } if *tlsBind != "" { - cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsPEM) + if *tlsKEY == "" { + tlsKEY = tlsPEM + } + + cert, err := tls.LoadX509KeyPair(*tlsPEM, *tlsKEY) + if err != nil { - log.Fatalf("Could 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}} From 58c7c41905e623cdfb985c799eac1670bdce22ba Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Sun, 22 Apr 2018 10:44:21 +0200 Subject: [PATCH 31/34] Remove unused constant --- goircd.go | 1 - 1 file changed, 1 deletion(-) diff --git a/goircd.go b/goircd.go index 32cfe78..c84dbec 100644 --- a/goircd.go +++ b/goircd.go @@ -39,7 +39,6 @@ import ( const ( PROXY_TIMEOUT = 5 - HEALTCHECK_PORT = 8080 ) var ( From c7c83926e1a4e07591e65603c5ed8eb3c986d51a Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Sun, 22 Apr 2018 10:45:02 +0200 Subject: [PATCH 32/34] Added argument (and env-var) to specify health-bind-address. --- goircd.go | 1 + 1 file changed, 1 insertion(+) diff --git a/goircd.go b/goircd.go index c84dbec..2725269 100644 --- a/goircd.go +++ b/goircd.go @@ -57,6 +57,7 @@ var ( 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.") clients_tls_total = prometheus.NewCounter( prometheus.CounterOpts{ From cf6cce277f39143cb0861c33920f2b692a67c8db Mon Sep 17 00:00:00 2001 From: Mathias Kaufmann Date: Sun, 22 Apr 2018 10:47:38 +0200 Subject: [PATCH 33/34] Use provided argument or fall-back for health-bind. The following arguments may used to specifiy health-check behavior: * `-healthcheck` : Enable healthcheck (default: `false`) * `-healthbind=[::1]:8086` : Override Healthcheck bind-address (default: `[::]:8086` Or use Environmentvariables `HEALTHCHECK` and `HEALTHBIND` --- goircd.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/goircd.go b/goircd.go index 2725269..a8b4576 100644 --- a/goircd.go +++ b/goircd.go @@ -207,13 +207,10 @@ func Run() { } func health_endpoint() { - var ( - health_bind = "0.0.0.0:8086" - ) health := healthchecking.NewHandler() health.AddLivenessCheck("goroutine-threshold", healthchecking.GoroutineCountCheck(100)) - log.Printf("Healthcheck listening on http://%s", health_bind) - http.ListenAndServe(health_bind, health) + log.Printf("Healthcheck listening on http://%s", *healtbind) + http.ListenAndServe(*healtbind, health) } func prom_export() { From 108909d03dfa2c4ad41832beea0076a26ad54639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Busse?= Date: Tue, 24 Apr 2018 21:14:36 +0200 Subject: [PATCH 34/34] Add steigr to AUTHORS --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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