From 7bf832322d2fca49b561a28f4613d398980e1584 Mon Sep 17 00:00:00 2001 From: "." <.@.> Date: Mon, 9 May 2016 18:48:07 +1200 Subject: [PATCH] hubsec nick opt, ctcp before join, delay join until ctcp version (with timeout), mirc/atomic-specific hack --HG-- branch : nmdc-ircfrontend --- main.go | 3 + server.go | 246 +++++++++++++++++++++++++++++----------------------- typedefs.go | 1 + 3 files changed, 140 insertions(+), 110 deletions(-) diff --git a/main.go b/main.go index 0415c3e..4dcc457 100644 --- a/main.go +++ b/main.go @@ -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() } } diff --git a/server.go b/server.go index 495ec89..9347304 100644 --- a/server.go +++ b/server.go @@ -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,7 +55,8 @@ type Server struct { verbose bool autojoin bool - sentCtcpVersion bool + recievedFirstServerMessage bool + recievedCtcpVersion bool } func NewServer(name string, upstream libnmdc.HubAddress, conn net.Conn) *Server { @@ -153,10 +157,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:], " ") @@ -177,7 +185,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) } } @@ -253,12 +261,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: @@ -356,7 +362,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}) } @@ -382,101 +390,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: 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 - } - } - - s.verbosef("Replacing client tag with '%s' version '%s'", tag, version) - - s.upstreamLauncher.Self.ClientTag = tag - s.upstreamLauncher.Self.ClientVersion = version - s.upstream.Hco.Self.ClientTag = tag - s.upstream.Hco.Self.ClientVersion = version - - 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) @@ -520,6 +433,119 @@ 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.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() { + s.ClientStateLock.Lock() + defer s.ClientStateLock.Unlock() + + 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" + + // 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 + } + } + + s.verbosef("Replacing client tag with '%s' version '%s'", tag, version) + + s.upstreamLauncher.Self.ClientTag = tag + s.upstreamLauncher.Self.ClientVersion = version + s.recievedCtcpVersion = true + + 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() diff --git a/typedefs.go b/typedefs.go index 9a56a62..e097f9d 100644 --- a/typedefs.go +++ b/typedefs.go @@ -27,6 +27,7 @@ const ( 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