hubsec nick opt, ctcp before join, delay join until ctcp version (with timeout), mirc/atomic-specific hack

--HG--
branch : nmdc-ircfrontend
This commit is contained in:
. 2016-05-09 18:48:07 +12:00
parent 169debea93
commit 7bf832322d
3 changed files with 140 additions and 110 deletions

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()
}
}

238
server.go
View File

@ -27,6 +27,7 @@ import (
"net"
"regexp"
"strings"
"sync"
"time"
)
@ -41,9 +42,11 @@ const (
type Server struct {
name string
motd string
hubSecNick string
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()

View File

@ -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