15 Commits

Author SHA1 Message Date
.
6a7847f71b preparations for 1.1.0 release
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:16:39 +12:00
.
7592bf6002 fix colon in mirc when renaming self
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:08:26 +12:00
.
fb34cefc46 fix crash on renaming self
--HG--
branch : nmdc-ircfrontend
2016-05-10 19:08:19 +12:00
.
0df6237005 strip leading v from mirc version
--HG--
branch : nmdc-ircfrontend
2016-05-10 18:56:32 +12:00
.
c35062a3df support changing nicks (by reconnecting)
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:57:04 +12:00
.
74e4f6cfa3 move mutex calls to outer function
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:49:00 +12:00
.
7bf832322d hubsec nick opt, ctcp before join, delay join until ctcp version (with timeout), mirc/atomic-specific hack
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:48:07 +12:00
.
169debea93 set proper clientversion, better version/tag extraction
--HG--
branch : nmdc-ircfrontend
2016-05-09 18:30:24 +12:00
.
9c2cad7986 doc update
--HG--
branch : nmdc-ircfrontend
2016-05-08 18:39:21 +12:00
.
32296eeffa doc update
--HG--
branch : nmdc-ircfrontend
2016-05-08 15:48:40 +12:00
.
3a00df8ef4 bugreport for mIRC 7
--HG--
branch : nmdc-ircfrontend
2016-05-08 15:26:05 +12:00
.
a948b457bd update readme
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:51:06 +12:00
.
11dbafe387 update readme
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:49:36 +12:00
.
a72f7be558 move readme file
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:43:42 +12:00
.
637bfd6c78 Added tag release-1.0.0 for changeset da295cede46d
--HG--
branch : nmdc-ircfrontend
2016-05-08 14:42:50 +12:00
6 changed files with 222 additions and 121 deletions

1
.hgtags Normal file
View File

@@ -0,0 +1 @@
da295cede46d95848348292e04e54fa5a5713ae3 release-1.0.0

View File

@@ -1,4 +1,11 @@
BUGS
====
- /wrong/ password message shows up, but not if no password was given for a passworded nick
WISHLIST
========
@@ -6,8 +13,6 @@ WISHLIST
- automatic markdown bold/italic formatting
- support changing nick (via reconnecting)
- support WHOIS
- respond to CTCP VERSION with the clienttag on behalf of other users
@@ -18,7 +23,6 @@ WISHLIST
WONTFIX
=======

View File

@@ -8,7 +8,7 @@ TLS (SSL) support is not integrated. To host the IRC frontend over TLS, please u
This program uses some code from the AGPLv3 project https://github.com/eXeC64/Rosella , from which it inherits the Affero GPLv3 license. Anyone hosting a modified version of this software is required to release their changes under the terms of the AGPLv3.
Tags: NMDC AGPLv3
Tags: nmdc, AGPLv3
=FEATURES=
@@ -18,6 +18,7 @@ Tags: NMDC AGPLv3
- NMDC server's title exposed as IRC channel topic
- Login with passworded nick (classic `PASS`, not `SASL`)
- Full nick list integration including op status
- Support IRC client changing nick via NMDC upstream reconnection
- Automatic join to single-enforced chatroom
- Multithreaded
- Single binary deployment
@@ -29,6 +30,8 @@ Tags: NMDC AGPLv3
Automatically join clients to the channel (default true)
-bind string
The address:port to bind to and listen for clients on (default ":6667")
-hubsecurity string
Nick used for administrative events (default "Hub-Security")
-servername string
Server name displayed to clients (default "nmdc-ircfrontend")
-upstream string
@@ -40,24 +43,33 @@ Tags: NMDC AGPLv3
NMDC's smaller community has standardised around comparatively few protocol implementations by means of necessity. In comparison, there are a lot of IRC client implementations with slightly differing interpretations of the protocol.
[b]Everything works:[/b]
Everything works:
- Hexchat
- Mango IRC
- AndroIRC
- Lite IRC
- Mutter
- Weechat
- mIRC 7
- HoloIRC (after version 4.1.0)
[b]Usable, with bugs:[/b]
- HoloIRC - Can't parse client tag, upstream bug https://github.com/tilal6991/HoloIRC/issues/140
Usable, with bugs:
- HoloIRC (4.1.0 and earlier) - Can't parse client tag, upstream bug https://github.com/tilal6991/HoloIRC/issues/140
- AndChat - Duplicate usernames appear, upstream bug https://github.com/znc/znc/issues/424
- Irssi - Ignorable warning "critical nicklist_set_host: assertion 'host != NULL' failed"
[b]Unusable:[/b]
- Yaaic - doesn't properly understand/parse the room join
- Atomic - doesn't properly understand/parse the room join, crashes on `PRIVMSG` with blank sender
Unusable:
- Yaaic and Atomic - doesn't properly understand/parse the room join
=CHANGELOG=
2016-05-10 1.1.0
- Feature: Support renaming own client during connection (`/nick`)
- Enhancement: Option to set Hub-Security nick (needed for initial CTCP, upgraded after upstream connection)
- Compatibility: Apply user-agent-specific hacks for mIRC and Atomic clients not nicely handling PRIVMSG with blank sender
- Compatibility: Better heuristic detection for client tag extraction
- Fix an issue causing the default client tag (nmdc-ircfrontend) to briefly appear in the NMDC user list, by (briefly) delaying the upstream join unless CTCP VERSION response comes in
- Fix an issue generating malformed version numbers in NMDC client tag
2016-05-08 1.0.0
- Initial public release

View File

@@ -30,8 +30,10 @@ func main() {
ircAddress := flag.String("bind", ":6667", "The address:port to bind to and listen for clients on")
dcAddress := flag.String("upstream", "127.0.0.1:411", "Upstream NMDC server")
serverName := flag.String("servername", "nmdc-ircfrontend", "Server name displayed to clients")
hubsec := flag.String("hubsecurity", "Hub-Security", "Nick used for administrative events")
verbose := flag.Bool("verbose", false, "Display debugging information")
autojoin := flag.Bool("autojoin", true, "Automatically join clients to the channel")
flag.Parse()
log.Printf("Listening on '%s'...", *ircAddress)
@@ -56,6 +58,7 @@ func main() {
server := NewServer(*serverName, libnmdc.HubAddress(*dcAddress), conn)
server.verbose = *verbose
server.autojoin = *autojoin
server.hubSecNick = *hubsec
go server.RunWorker()
}
}

298
server.go
View File

@@ -27,6 +27,7 @@ import (
"net"
"regexp"
"strings"
"sync"
"time"
)
@@ -39,11 +40,13 @@ const (
)
type Server struct {
name string
motd string
name string
motd string
hubSecNick string
clientConn net.Conn
clientState ClientState
clientConn net.Conn
clientState ClientState
ClientStateLock sync.Mutex
upstreamLauncher libnmdc.HubConnectionOptions
upstreamCloser chan struct{}
@@ -52,12 +55,15 @@ type Server struct {
verbose bool
autojoin bool
sentCtcpVersion bool
recievedFirstServerMessage bool
recievedCtcpVersion bool
nickChangeAttempt int
}
func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server {
self := libnmdc.NewUserInfo("")
self.ClientTag = APP_DESCRIPTION
self.ClientTag = APP_NAME
self.ClientVersion = APP_VERSION
return &Server{
name: name,
@@ -152,10 +158,14 @@ func (s *Server) RunWorker() {
func (s *Server) postGeneralMessageInRoom(msg string) {
// FIXME blank names work well in hexchat, but not in yaaic
var BLANK_NICK = "" // "!@" + s.name // "PtokaX" // "\xC2\xA0"
var sendAs = ""
// CTCP action conversion
// Some clients can't handle blank nicks very well
if s.upstreamLauncher.Self.ClientTag == "mIRC" || s.upstreamLauncher.Self.ClientTag == "Atomic" {
sendAs = s.hubSecNick + "!" + s.hubSecNick + "@" + s.hubSecNick
}
// Detect pseudo-system message for potential CTCP ACTION conversion
words := strings.Split(msg, " ")
firstWord := words[0]
remainder := strings.Join(words[1:], " ")
@@ -176,7 +186,7 @@ func (s *Server) postGeneralMessageInRoom(msg string) {
} else {
// genuine system message
s.reply(rplMsg, BLANK_NICK, BLESSED_CHANNEL, msg)
s.reply(rplMsg, sendAs, BLESSED_CHANNEL, msg)
}
}
@@ -236,8 +246,18 @@ func (s *Server) upstreamWorker() {
s.sendNames() // delay doing this until now
}
if hubEvent.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED {
// Abandon thread. Don't try to autoreconnect at our level, the remote client can be responsible for that
s.DisconnectClient()
if s.nickChangeAttempt > 0 {
// If this was a nick change, reconnect /immediately/
s.upstream = nil
s.clientState = CSRegistered
s.maybeStartUpstream() // launches new goroutine
} else {
// Abandon thread. Don't try to autoreconnect at our level, the remote client can be responsible for that
s.DisconnectClient()
}
return
}
case libnmdc.EVENT_HUBNAME_CHANGED:
@@ -252,12 +272,10 @@ func (s *Server) upstreamWorker() {
} else {
// nick!username@userhost, but for us all three of those are always identical
s.reply(rplMsg, hubEvent.Nick+"!"+hubEvent.Nick+"@"+hubEvent.Nick, BLESSED_CHANNEL, reformatIncomingMessageBody(hubEvent.Message))
}
if !s.sentCtcpVersion {
// Abuse this first username in order to also send a CTCP version request
s.reply(rplMsg, hubEvent.Nick+"!"+hubEvent.Nick+"@"+hubEvent.Nick, BLESSED_CHANNEL, "\x01VERSION\x01")
s.sentCtcpVersion = true
}
if !s.recievedFirstServerMessage {
s.hubSecNick = hubEvent.Nick // Replace with the hub's real Hub-Security nick, although we shouldn't need it again
}
case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB:
@@ -280,10 +298,10 @@ func (s *Server) handleCommand(command string, args []string) {
// do nothing
case "INFO":
s.reply(rplInfo, APP_DESCRIPTION)
s.reply(rplInfo, APP_NAME+" v"+APP_VERSION)
case "VERSION":
s.reply(rplVersion, VERSION)
s.reply(rplVersion, APP_VERSION)
case "MOTD":
s.sendMOTD(s.motd)
@@ -317,17 +335,38 @@ func (s *Server) handleCommand(command string, args []string) {
return
}
// mIRC puts a colon in first place when changing nick, hexchat doesn't
suppliedNick := args[0]
if len(suppliedNick) >= 2 && suppliedNick[0] == ':' {
suppliedNick = suppliedNick[1:]
}
if s.clientNick() == "" {
// allow set, as part of the login phase
s.upstreamLauncher.Self.Nick = args[0]
s.upstreamLauncher.Self.Nick = suppliedNick
} else if args[0] == s.clientNick() {
} else if suppliedNick == s.clientNick() {
// Ignore
// Required for compatibility with Lite IRC, which sends USER/NICK in the wrong order
} else {
s.reply(rplKill, "Can't change nicks on this server.")
s.DisconnectClient()
s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
if s.upstream == nil {
// Not yet connected, should be safe to change nick
s.upstreamLauncher.Self.Nick = suppliedNick
} else {
// Need to disconnect/reconnect the upstream
s.writeClient(fmt.Sprintf(":%s!%s@%s NICK %s", s.clientNick(), s.clientNick(), s.clientNick(), suppliedNick)) // notify client about what they've done
s.upstreamLauncher.Self.Nick = suppliedNick
s.nickChangeAttempt++
s.upstream.Disconnect()
}
//s.reply(rplKill, "Can't change nicks on this server.")
//s.DisconnectClient()
}
case "USER":
@@ -355,7 +394,9 @@ func (s *Server) handleCommand(command string, args []string) {
s.reply(rplWelcome)
s.clientState = CSRegistered
// TODO tell the client that they /must/ join #chat
// Send CTCP VERSION request immediately
s.reply(rplMsg, s.hubSecNick+"!"+s.hubSecNick+"@"+s.hubSecNick, BLESSED_CHANNEL, "\x01VERSION\x01")
if s.autojoin {
s.handleCommand("JOIN", []string{BLESSED_CHANNEL})
}
@@ -381,91 +422,6 @@ func (s *Server) handleRegisteredCommand(command string, args []string) {
}
s.reply(rplListEnd)
case "JOIN":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
switch args[0] {
case BLESSED_CHANNEL:
if s.clientState != CSJoined {
// Join for the first time
s.clientState = CSJoined
// Acknowledge
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
// Send (initially just us) nicklist for the chat channel
//s.sendNames()
// Spawn upstream connection
s.upstream = s.upstreamLauncher.Connect()
go s.upstreamWorker()
} else {
// They're already here, ignore
// Can happen if autojoin is enabled but the client already requested a login
}
case "0":
// Quitting all channels? Drop client
s.reply(rplKill, "Bye.")
s.DisconnectClient()
default:
s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.")
s.DisconnectClient()
}
default:
s.handleJoinedCommand(command, args)
}
}
func (s *Server) SetClientSoftwareVersion(ver string) {
// "HexChat 2.12.1 [x64] / Microsoft Windows 10 Pro (x64) [Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz (3.60GHz)]"
// "AndroIRC - Android IRC Client (5.2 - Build 6830152) - http://www.androirc.com"
// "AndChat 1.4.3.2 http://www.andchat.net"
// "liteIRC for Android 1.1.8"
// ":Relay:1.0:Android"
// A bit long and unwieldy. Heuristic: stop after the first word containing digits
parts := regexp.MustCompilePOSIX("^([^0-9]*[0-9]+[^ ]*) ").FindStringSubmatch(ver)
if len(parts) > 0 {
ver = strings.TrimRight(parts[0], " ")
}
s.verbosef("Replacing client tag with '%s'...", ver)
s.upstreamLauncher.Self.ClientTag = ver
s.upstream.Hco.Self.ClientTag = ver
s.upstream.SayInfo()
}
func (s *Server) handleJoinedCommand(command string, args []string) {
if s.clientState != CSJoined {
s.reply(errNotReg)
return
}
switch command {
case "PART":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
if args[0] == BLESSED_CHANNEL {
// You can check out any time you like, but you can never leave
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
s.sendNames()
}
case "PRIVMSG", "NOTICE":
if len(args) < 2 {
s.reply(errMoreArgs)
@@ -509,6 +465,130 @@ func (s *Server) handleJoinedCommand(command string, args []string) {
}
case "JOIN":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
switch args[0] {
case BLESSED_CHANNEL:
// Give it a few seconds - it's better to hear the CTCP VERSION
// response first. Once we get that, it'll connect instantly
go func() {
<-time.After(WAIT_FOR_VERSION * time.Second)
s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
s.maybeStartUpstream()
}()
case "0":
// Quitting all channels? Drop client
s.reply(rplKill, "Bye.")
s.DisconnectClient()
default:
s.reply(rplKill, "There is only '"+BLESSED_CHANNEL+"'.")
s.DisconnectClient()
}
default:
s.handleJoinedCommand(command, args)
}
}
func (s *Server) maybeStartUpstream() {
if s.clientState != CSJoined {
// Join for the first time
s.clientState = CSJoined
// Acknowledge
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
// Spawn upstream connection
s.upstream = s.upstreamLauncher.Connect()
go s.upstreamWorker()
} else {
// They're already here, ignore
// Can happen if autojoin is enabled but the client already requested a login
}
}
func (s *Server) SetClientSoftwareVersion(ver string) {
// "HexChat 2.12.1 [x64] / Microsoft Windows 10 Pro (x64) [Intel(R) Core(TM) i5-2500 CPU @ 3.30GHz (3.60GHz)]"
// "AndroIRC - Android IRC Client (5.2 - Build 6830152) - http://www.androirc.com"
// "AndChat 1.4.3.2 http://www.andchat.net"
// "liteIRC for Android 1.1.8"
// ":Relay:1.0:Android"
// "mIRC v7.45"
// A bit long and unwieldy.
// Heuristic: keep the first word, and the the first word containing digits
tag := APP_NAME
version := APP_VERSION
words := strings.Split(ver, " ")
for _, word := range words[1:] {
if strings.ContainsAny(word, "0123456789") {
tag = words[0]
version = strings.Replace(word, "(", "", -1) // AndroIRC
break
}
}
// Strip leading v from mIRC
if len(version) >= 2 && (version[0] == 'v' || version[0] == 'V') && strings.ContainsAny(string(version[1]), "0123456789") {
version = version[1:]
}
s.verbosef("Replacing client tag with '%s' version '%s'", tag, version)
s.upstreamLauncher.Self.ClientTag = tag
s.upstreamLauncher.Self.ClientVersion = version
s.recievedCtcpVersion = true
s.ClientStateLock.Lock()
defer s.ClientStateLock.Unlock()
if s.upstream != nil {
s.upstream.Hco.Self.ClientTag = tag
s.upstream.Hco.Self.ClientVersion = version
s.upstream.SayInfo()
} else {
// Connected for the first time (order was CTCP VERSION --> JOIN)
s.maybeStartUpstream()
}
}
func (s *Server) handleJoinedCommand(command string, args []string) {
if s.clientState != CSJoined {
s.reply(errNotReg)
return
}
switch command {
case "PART":
if len(args) < 1 {
s.reply(errMoreArgs)
return
}
if args[0] == BLESSED_CHANNEL {
// You can check out any time you like, but you can never leave
s.reply(rplJoin, s.clientNick(), BLESSED_CHANNEL)
s.sendNames()
}
case "QUIT":
s.DisconnectClient()

View File

@@ -19,14 +19,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const (
VERSION = "1.0.0"
APP_DESCRIPTION = "nmdc-ircfrontend v" + VERSION
APP_VERSION = "1.1.0"
APP_NAME = "nmdc-ircfrontend"
BLESSED_CHANNEL = "#chat" // must be lowercase
BLESSED_CHANNEL_MODE = "n" // means that you have to be in the channel to chat, but that's it
NICKS_PER_PROTOMSG = 128 // Max number of nicks to send per message
CLIENT_READ_BUFFSIZE = 512
CLIENT_KEEPALIVE_EVERY = 60 // should be longer than the client's one, hexchat is 30s
WAIT_FOR_VERSION = 3 // Seconds to wait for the CTCP VERSION response before connecting with default MyINFO
)
type replyCode int