move all nmdc-specific content to a separate NmdcProtocol class
--HG-- branch : adc
This commit is contained in:
parent
276ece1cf1
commit
7543d7058f
@ -27,9 +27,22 @@ func (this *HubAddress) parse() url.URL {
|
||||
func (this *HubAddress) IsSecure() bool {
|
||||
parsed := this.parse()
|
||||
|
||||
return parsed.Scheme == "nmdcs" || parsed.Scheme == "dchubs"
|
||||
return parsed.Scheme == "nmdcs" || parsed.Scheme == "dchubs" || parsed.Scheme == "adcs"
|
||||
}
|
||||
|
||||
func (this *HubAddress) GetHostOnly() string {
|
||||
return this.parse().Host
|
||||
}
|
||||
|
||||
func (this *HubAddress) GetProtocol() HubProtocol {
|
||||
parsed := this.parse()
|
||||
|
||||
switch parsed.Scheme {
|
||||
case "nmdc", "dchub", "nmdcs", "dchubs":
|
||||
return HubProtocolNmdc
|
||||
case "adc", "adcs":
|
||||
return HubProtocolAdc
|
||||
default:
|
||||
return HubProtocolAutodetect
|
||||
}
|
||||
}
|
||||
|
269
HubConnection.go
269
HubConnection.go
@ -4,8 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@ -20,6 +19,8 @@ type HubConnection struct {
|
||||
users map[string]UserInfo
|
||||
userLock sync.RWMutex
|
||||
|
||||
proto Protocol
|
||||
|
||||
// Streamed events
|
||||
processEvent func(HubEvent)
|
||||
OnEvent chan HubEvent
|
||||
@ -27,11 +28,8 @@ type HubConnection struct {
|
||||
// Private state
|
||||
conn net.Conn // this is an interface
|
||||
connValid bool
|
||||
sentOurHello bool
|
||||
autoReconnect bool
|
||||
lastDataRecieved time.Time
|
||||
|
||||
supports map[string]struct{}
|
||||
}
|
||||
|
||||
// Thread-safe user accessor.
|
||||
@ -43,15 +41,11 @@ func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error {
|
||||
}
|
||||
|
||||
func (this *HubConnection) SayPublic(message string) {
|
||||
this.SayRaw("<" + this.Hco.Self.Nick + "> " + Escape(message) + "|")
|
||||
this.proto.SayPublic(message)
|
||||
}
|
||||
|
||||
func (this *HubConnection) SayPrivate(recipient string, message string) {
|
||||
this.SayRaw("$To: " + recipient + " From: " + this.Hco.Self.Nick + " $<" + this.Hco.Self.Nick + "> " + Escape(message) + "|")
|
||||
}
|
||||
|
||||
func (this *HubConnection) SayInfo() {
|
||||
this.SayRaw(this.Hco.Self.toMyINFO() + "|")
|
||||
this.proto.SayPrivate(recipient, message)
|
||||
}
|
||||
|
||||
func (this *HubConnection) UserExists(nick string) bool {
|
||||
@ -99,250 +93,12 @@ func (this *HubConnection) userJoined_Full(uinf *UserInfo) {
|
||||
// Note that protocol messages are transmitted on the caller thread, not from
|
||||
// any internal libnmdc thread.
|
||||
func (this *HubConnection) SayRaw(protocolCommand string) error {
|
||||
if this.connValid {
|
||||
_, err := this.conn.Write([]byte(protocolCommand))
|
||||
return err
|
||||
} else {
|
||||
if !this.connValid {
|
||||
return ErrNotConnected
|
||||
}
|
||||
}
|
||||
|
||||
func (this *HubConnection) processProtocolMessage(message string) {
|
||||
|
||||
// Zero-length protocol message
|
||||
// ````````````````````````````
|
||||
if len(message) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Public chat
|
||||
// ```````````
|
||||
if rx_publicChat.MatchString(message) {
|
||||
pubchat_parts := rx_publicChat.FindStringSubmatch(message)
|
||||
this.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: Unescape(pubchat_parts[2])})
|
||||
return
|
||||
}
|
||||
|
||||
// System messages
|
||||
// ```````````````
|
||||
if message[0] != '$' {
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.HubName, Message: Unescape(message)})
|
||||
return
|
||||
}
|
||||
|
||||
// Protocol messages
|
||||
// `````````````````
|
||||
|
||||
commandParts := strings.SplitN(message, " ", 2)
|
||||
switch commandParts[0] {
|
||||
|
||||
case "$Lock":
|
||||
this.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" +
|
||||
"$Key " + unlock([]byte(commandParts[1])) + "|")
|
||||
this.sentOurHello = false
|
||||
|
||||
case "$Hello":
|
||||
if commandParts[1] == this.Hco.Self.Nick && !this.sentOurHello {
|
||||
this.SayRaw("$Version 1,0091|")
|
||||
this.SayRaw("$GetNickList|")
|
||||
this.SayInfo()
|
||||
this.sentOurHello = true
|
||||
|
||||
} else {
|
||||
this.userJoined_NameOnly(commandParts[1])
|
||||
|
||||
}
|
||||
|
||||
case "$HubName":
|
||||
this.HubName = commandParts[1]
|
||||
this.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]})
|
||||
|
||||
case "$ValidateDenide": // sic
|
||||
if len(this.Hco.NickPassword) > 0 {
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
|
||||
} else {
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."})
|
||||
}
|
||||
|
||||
case "$HubIsFull":
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."})
|
||||
|
||||
case "$BadPass":
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
|
||||
|
||||
case "$GetPass":
|
||||
if len(this.Hco.NickPassword) == 0 {
|
||||
// We've got a problem. MyPass with no arguments is a syntax error with no message = instant close
|
||||
// Just drop the connection
|
||||
this.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."})
|
||||
this.Disconnect()
|
||||
} else {
|
||||
this.SayRaw("$MyPass " + Escape(this.Hco.NickPassword) + "|")
|
||||
}
|
||||
|
||||
case "$Quit":
|
||||
this.userLock.Lock()
|
||||
delete(this.users, commandParts[1])
|
||||
this.userLock.Unlock() // Don't lock over a processEvent boundary
|
||||
|
||||
this.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]})
|
||||
|
||||
case "$MyINFO":
|
||||
u := UserInfo{}
|
||||
err := u.fromMyINFO(commandParts[1])
|
||||
if err != nil {
|
||||
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
this.userJoined_Full(&u)
|
||||
|
||||
case "$NickList":
|
||||
nicklist := strings.Split(commandParts[1], "$$")
|
||||
for _, nick := range nicklist {
|
||||
if len(nick) > 0 {
|
||||
this.userJoined_NameOnly(nick)
|
||||
}
|
||||
}
|
||||
|
||||
case "$OpList":
|
||||
oplist := strings.Split(commandParts[1], "$$")
|
||||
opmap := map[string]struct{}{}
|
||||
|
||||
// Organise/sort the list, and ensure we're not meeting an operator for
|
||||
// the first time
|
||||
for _, nick := range oplist {
|
||||
if len(nick) > 0 {
|
||||
opmap[nick] = struct{}{}
|
||||
this.userJoined_NameOnly(nick) // assert existence; noop otherwise
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all mentioned nicks as being operators, and all unmentioned nicks
|
||||
// as being /not/ an operator. (second pass minimises RW mutex use)
|
||||
func() {
|
||||
this.userLock.Lock()
|
||||
defer this.userLock.Unlock()
|
||||
|
||||
for nick, userinfo := range this.users {
|
||||
_, isop := opmap[nick]
|
||||
|
||||
userinfo.IsOperator = isop
|
||||
this.users[nick] = userinfo
|
||||
}
|
||||
}()
|
||||
|
||||
case "$To:":
|
||||
valid := false
|
||||
if rx_incomingTo.MatchString(commandParts[1]) {
|
||||
txparts := rx_incomingTo.FindStringSubmatch(commandParts[1])
|
||||
if txparts[1] == this.Hco.Self.Nick && txparts[2] == txparts[3] {
|
||||
this.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: Unescape(txparts[4])})
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"})
|
||||
}
|
||||
|
||||
case "$UserIP":
|
||||
this.userLock.Lock()
|
||||
|
||||
pairs := strings.Split(commandParts[1], "$$")
|
||||
notifyOfUpdate := make([]string, 0, len(pairs))
|
||||
|
||||
nextIPPair:
|
||||
for _, pair := range pairs {
|
||||
parts := strings.SplitN(pair, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
// ????
|
||||
continue nextIPPair
|
||||
}
|
||||
|
||||
ip2nick := parts[0]
|
||||
ip2addr := parts[1]
|
||||
|
||||
uinfo, ok := this.users[ip2nick]
|
||||
if !ok {
|
||||
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"})
|
||||
continue nextIPPair
|
||||
}
|
||||
|
||||
if uinfo.IPAddress != ip2addr {
|
||||
uinfo.IPAddress = ip2addr
|
||||
notifyOfUpdate = append(notifyOfUpdate, ip2nick)
|
||||
this.users[ip2nick] = uinfo
|
||||
}
|
||||
}
|
||||
|
||||
this.userLock.Unlock()
|
||||
|
||||
for _, nick := range notifyOfUpdate {
|
||||
this.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick})
|
||||
}
|
||||
|
||||
case "$ForceMove":
|
||||
this.Hco.Address = HubAddress(commandParts[1])
|
||||
this.conn.Close() // we'll reconnect onto the new address
|
||||
|
||||
case "$UserCommand":
|
||||
// $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new||
|
||||
if rx_userCommand.MatchString(commandParts[1]) {
|
||||
usc := rx_userCommand.FindStringSubmatch(commandParts[1])
|
||||
|
||||
typeInt, _ := strconv.Atoi(usc[1])
|
||||
contextInt, _ := strconv.Atoi(usc[2])
|
||||
|
||||
uscStruct := UserCommand{
|
||||
Type: UserCommandType(typeInt),
|
||||
Context: UserCommandContext(contextInt),
|
||||
Message: usc[3],
|
||||
Command: Unescape(usc[4]),
|
||||
}
|
||||
|
||||
this.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct})
|
||||
|
||||
} else {
|
||||
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"})
|
||||
}
|
||||
|
||||
case "$Supports":
|
||||
this.supports = make(map[string]struct{})
|
||||
for _, s := range strings.Split(commandParts[1], " ") {
|
||||
this.supports[s] = struct{}{}
|
||||
}
|
||||
|
||||
if !this.sentOurHello {
|
||||
|
||||
// Need to log in.
|
||||
// If the hub supports QuickList, we can skip one network roundtrip
|
||||
if _, ok := this.supports["QuickList"]; ok {
|
||||
this.SayInfo()
|
||||
this.SayRaw("$GetNickList|")
|
||||
} else {
|
||||
this.SayRaw("$ValidateNick " + Escape(this.Hco.Self.Nick) + "|")
|
||||
}
|
||||
|
||||
// This also counts as the end of the handshake from our POV. Consider
|
||||
// ourselves logged in
|
||||
this.sentOurHello = true
|
||||
if this.State != CONNECTIONSTATE_CONNECTED {
|
||||
this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
|
||||
this.State = CONNECTIONSTATE_CONNECTED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// IGNORABLE COMMANDS
|
||||
case "$HubTopic":
|
||||
case "$Search":
|
||||
case "$ConnectToMe":
|
||||
|
||||
default:
|
||||
this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"})
|
||||
|
||||
}
|
||||
_, err := this.conn.Write([]byte(protocolCommand))
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *HubConnection) Disconnect() {
|
||||
@ -396,7 +152,7 @@ func (this *HubConnection) worker() {
|
||||
err = nil
|
||||
|
||||
// Send KA packet
|
||||
_, err = this.conn.Write([]byte("|"))
|
||||
err = this.proto.SayKeepalive()
|
||||
}
|
||||
|
||||
if nbytes > 0 {
|
||||
@ -405,14 +161,17 @@ func (this *HubConnection) worker() {
|
||||
}
|
||||
}
|
||||
|
||||
rxSeparator := regexp.QuoteMeta(this.proto.ProtoMessageSeparator())
|
||||
rxProtocolMessage := regexp.MustCompile(`(?ms)\A[^` + rxSeparator + `]*` + rxSeparator)
|
||||
|
||||
// Attempt to parse a message block
|
||||
for len(fullBuffer) > 0 {
|
||||
for len(fullBuffer) > 0 && fullBuffer[0] == '|' {
|
||||
fullBuffer = fullBuffer[1:]
|
||||
}
|
||||
protocolMessage := rx_protocolMessage.FindString(fullBuffer)
|
||||
protocolMessage := rxProtocolMessage.FindString(fullBuffer)
|
||||
if len(protocolMessage) > 0 {
|
||||
this.processProtocolMessage(protocolMessage[:len(protocolMessage)-1])
|
||||
this.proto.ProcessCommand(protocolMessage[:len(protocolMessage)-1])
|
||||
fullBuffer = fullBuffer[len(protocolMessage):]
|
||||
} else {
|
||||
break
|
||||
|
413
NmdcProtocol.go
Normal file
413
NmdcProtocol.go
Normal file
@ -0,0 +1,413 @@
|
||||
package libnmdc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NmdcProtocol struct {
|
||||
hc *HubConnection
|
||||
sentOurHello bool
|
||||
supports map[string]struct{}
|
||||
|
||||
rxPublicChat *regexp.Regexp
|
||||
rxIncomingTo *regexp.Regexp
|
||||
rxUserCommand *regexp.Regexp
|
||||
rxMyInfo *regexp.Regexp
|
||||
rxMyInfoNoTag *regexp.Regexp
|
||||
}
|
||||
|
||||
var _ Protocol = &NmdcProtocol{} // Assert that we implement the interface
|
||||
|
||||
func NewNmdcProtocol(hc *HubConnection) *NmdcProtocol {
|
||||
proto := NmdcProtocol{}
|
||||
proto.hc = hc
|
||||
|
||||
// With the `m` flag, use \A instead of ^ to anchor to start
|
||||
// This fixes accidentally finding a better match in the middle of a multi-line message
|
||||
|
||||
proto.rxPublicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`)
|
||||
proto.rxIncomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`)
|
||||
proto.rxUserCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`)
|
||||
|
||||
// Format: $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$
|
||||
|
||||
HEAD := `(?ms)^\$ALL ([^ ]+) `
|
||||
FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$`
|
||||
|
||||
proto.rxMyInfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT)
|
||||
proto.rxMyInfoNoTag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag
|
||||
|
||||
// Done
|
||||
return &proto
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) ProcessCommand(message string) {
|
||||
|
||||
// Zero-length protocol message
|
||||
// ````````````````````````````
|
||||
if len(message) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Public chat
|
||||
// ```````````
|
||||
if this.rxPublicChat.MatchString(message) {
|
||||
pubchat_parts := this.rxPublicChat.FindStringSubmatch(message)
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: this.Unescape(pubchat_parts[2])})
|
||||
return
|
||||
}
|
||||
|
||||
// System messages
|
||||
// ```````````````
|
||||
if message[0] != '$' {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.hc.HubName, Message: this.Unescape(message)})
|
||||
return
|
||||
}
|
||||
|
||||
// Protocol messages
|
||||
// `````````````````
|
||||
|
||||
commandParts := strings.SplitN(message, " ", 2)
|
||||
switch commandParts[0] {
|
||||
|
||||
case "$Lock":
|
||||
this.hc.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" +
|
||||
"$Key " + this.unlock([]byte(commandParts[1])) + "|")
|
||||
this.sentOurHello = false
|
||||
|
||||
case "$Hello":
|
||||
if commandParts[1] == this.hc.Hco.Self.Nick && !this.sentOurHello {
|
||||
this.hc.SayRaw("$Version 1,0091|")
|
||||
this.hc.SayRaw("$GetNickList|")
|
||||
this.sayInfo()
|
||||
this.sentOurHello = true
|
||||
|
||||
} else {
|
||||
this.hc.userJoined_NameOnly(commandParts[1])
|
||||
|
||||
}
|
||||
|
||||
case "$HubName":
|
||||
this.hc.HubName = commandParts[1]
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]})
|
||||
|
||||
case "$ValidateDenide": // sic
|
||||
if len(this.hc.Hco.NickPassword) > 0 {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
|
||||
} else {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."})
|
||||
}
|
||||
|
||||
case "$HubIsFull":
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."})
|
||||
|
||||
case "$BadPass":
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."})
|
||||
|
||||
case "$GetPass":
|
||||
if len(this.hc.Hco.NickPassword) == 0 {
|
||||
// We've got a problem. MyPass with no arguments is a syntax error with no message = instant close
|
||||
// Just drop the connection
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."})
|
||||
this.hc.Disconnect()
|
||||
} else {
|
||||
this.hc.SayRaw("$MyPass " + this.Escape(this.hc.Hco.NickPassword) + "|")
|
||||
}
|
||||
|
||||
case "$Quit":
|
||||
this.hc.userLock.Lock()
|
||||
delete(this.hc.users, commandParts[1])
|
||||
this.hc.userLock.Unlock() // Don't lock over a processEvent boundary
|
||||
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]})
|
||||
|
||||
case "$MyINFO":
|
||||
u, err := this.parseMyINFO(commandParts[1])
|
||||
if err != nil {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
this.hc.userJoined_Full(u)
|
||||
|
||||
case "$NickList":
|
||||
nicklist := strings.Split(commandParts[1], "$$")
|
||||
for _, nick := range nicklist {
|
||||
if len(nick) > 0 {
|
||||
this.hc.userJoined_NameOnly(nick)
|
||||
}
|
||||
}
|
||||
|
||||
case "$OpList":
|
||||
oplist := strings.Split(commandParts[1], "$$")
|
||||
opmap := map[string]struct{}{}
|
||||
|
||||
// Organise/sort the list, and ensure we're not meeting an operator for
|
||||
// the first time
|
||||
for _, nick := range oplist {
|
||||
if len(nick) > 0 {
|
||||
opmap[nick] = struct{}{}
|
||||
this.hc.userJoined_NameOnly(nick) // assert existence; noop otherwise
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all mentioned nicks as being operators, and all unmentioned nicks
|
||||
// as being /not/ an operator. (second pass minimises RW mutex use)
|
||||
func() {
|
||||
this.hc.userLock.Lock()
|
||||
defer this.hc.userLock.Unlock()
|
||||
|
||||
for nick, userinfo := range this.hc.users {
|
||||
_, isop := opmap[nick]
|
||||
|
||||
userinfo.IsOperator = isop
|
||||
this.hc.users[nick] = userinfo
|
||||
}
|
||||
}()
|
||||
|
||||
case "$To:":
|
||||
valid := false
|
||||
if this.rxIncomingTo.MatchString(commandParts[1]) {
|
||||
txparts := this.rxIncomingTo.FindStringSubmatch(commandParts[1])
|
||||
if txparts[1] == this.hc.Hco.Self.Nick && txparts[2] == txparts[3] {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: this.Unescape(txparts[4])})
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"})
|
||||
}
|
||||
|
||||
case "$UserIP":
|
||||
this.hc.userLock.Lock()
|
||||
|
||||
pairs := strings.Split(commandParts[1], "$$")
|
||||
notifyOfUpdate := make([]string, 0, len(pairs))
|
||||
|
||||
nextIPPair:
|
||||
for _, pair := range pairs {
|
||||
parts := strings.SplitN(pair, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
// ????
|
||||
continue nextIPPair
|
||||
}
|
||||
|
||||
ip2nick := parts[0]
|
||||
ip2addr := parts[1]
|
||||
|
||||
uinfo, ok := this.hc.users[ip2nick]
|
||||
if !ok {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"})
|
||||
continue nextIPPair
|
||||
}
|
||||
|
||||
if uinfo.IPAddress != ip2addr {
|
||||
uinfo.IPAddress = ip2addr
|
||||
notifyOfUpdate = append(notifyOfUpdate, ip2nick)
|
||||
this.hc.users[ip2nick] = uinfo
|
||||
}
|
||||
}
|
||||
|
||||
this.hc.userLock.Unlock()
|
||||
|
||||
for _, nick := range notifyOfUpdate {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick})
|
||||
}
|
||||
|
||||
case "$ForceMove":
|
||||
this.hc.Hco.Address = HubAddress(commandParts[1])
|
||||
this.hc.conn.Close() // we'll reconnect onto the new address
|
||||
|
||||
case "$UserCommand":
|
||||
// $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new||
|
||||
if this.rxUserCommand.MatchString(commandParts[1]) {
|
||||
usc := this.rxUserCommand.FindStringSubmatch(commandParts[1])
|
||||
|
||||
typeInt, _ := strconv.Atoi(usc[1])
|
||||
contextInt, _ := strconv.Atoi(usc[2])
|
||||
|
||||
uscStruct := UserCommand{
|
||||
Type: UserCommandType(typeInt),
|
||||
Context: UserCommandContext(contextInt),
|
||||
Message: usc[3],
|
||||
Command: this.Unescape(usc[4]),
|
||||
}
|
||||
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct})
|
||||
|
||||
} else {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"})
|
||||
}
|
||||
|
||||
case "$Supports":
|
||||
this.supports = make(map[string]struct{})
|
||||
for _, s := range strings.Split(commandParts[1], " ") {
|
||||
this.supports[s] = struct{}{}
|
||||
}
|
||||
|
||||
if !this.sentOurHello {
|
||||
|
||||
// Need to log in.
|
||||
// If the hub supports QuickList, we can skip one network roundtrip
|
||||
if _, ok := this.supports["QuickList"]; ok {
|
||||
this.sayInfo()
|
||||
this.hc.SayRaw("$GetNickList|")
|
||||
} else {
|
||||
this.hc.SayRaw("$ValidateNick " + this.Escape(this.hc.Hco.Self.Nick) + "|")
|
||||
}
|
||||
|
||||
// This also counts as the end of the handshake from our POV. Consider
|
||||
// ourselves logged in
|
||||
this.sentOurHello = true
|
||||
if this.hc.State != CONNECTIONSTATE_CONNECTED {
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
|
||||
this.hc.State = CONNECTIONSTATE_CONNECTED
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// IGNORABLE COMMANDS
|
||||
case "$HubTopic":
|
||||
case "$Search":
|
||||
case "$ConnectToMe":
|
||||
|
||||
default:
|
||||
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) Escape(plaintext string) string {
|
||||
v1 := strings.Replace(plaintext, "&", "&", -1)
|
||||
v2 := strings.Replace(v1, "|", "|", -1)
|
||||
return strings.Replace(v2, "$", "$", -1)
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) Unescape(encoded string) string {
|
||||
v1 := strings.Replace(encoded, "$", "$", -1)
|
||||
v2 := strings.Replace(v1, "|", "|", -1)
|
||||
return strings.Replace(v2, "&", "&", -1)
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) SayPublic(message string) {
|
||||
this.hc.SayRaw("<" + this.hc.Hco.Self.Nick + "> " + this.Escape(message) + "|")
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) SayPrivate(recipient, message string) {
|
||||
this.hc.SayRaw("$To: " + recipient + " From: " + this.hc.Hco.Self.Nick + " $<" + this.hc.Hco.Self.Nick + "> " + this.Escape(message) + "|")
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) sayInfo() {
|
||||
this.hc.SayRaw(this.getUserMyINFO(&this.hc.Hco.Self) + "|")
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) parseMyINFO(protomsg string) (*UserInfo, error) {
|
||||
ret := UserInfo{}
|
||||
|
||||
// Normal format (with tag in exact V/M/H/S order)
|
||||
matches := this.rxMyInfo.FindStringSubmatch(protomsg)
|
||||
if matches != nil {
|
||||
ret.Nick = matches[1]
|
||||
ret.Description = this.Unescape(matches[2])
|
||||
ret.ClientTag = this.Unescape(matches[3])
|
||||
ret.ClientVersion = matches[4]
|
||||
ret.ConnectionMode = ConnectionMode(matches[5][0])
|
||||
maybeParse(matches[6], &ret.HubsUnregistered, 0)
|
||||
maybeParse(matches[7], &ret.HubsRegistered, 0)
|
||||
maybeParse(matches[8], &ret.HubsOperator, 0)
|
||||
maybeParse(matches[9], &ret.Slots, 0)
|
||||
if len(matches[10]) > 1 {
|
||||
ret.Speed = matches[10][:len(matches[10])-2]
|
||||
} else {
|
||||
ret.Speed = ""
|
||||
}
|
||||
ret.Flag = UserFlag(matches[10][len(matches[10])-1])
|
||||
ret.Email = this.Unescape(matches[11])
|
||||
maybeParse(matches[12], &ret.ShareSize, 0)
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// No-tag format, used in early connection
|
||||
matches = this.rxMyInfoNoTag.FindStringSubmatch(protomsg)
|
||||
if matches != nil {
|
||||
ret.Nick = matches[1]
|
||||
ret.Description = this.Unescape(matches[2])
|
||||
ret.ClientTag = ""
|
||||
ret.ClientVersion = "0"
|
||||
ret.ConnectionMode = CONNECTIONMODE_PASSIVE
|
||||
ret.HubsUnregistered = 0
|
||||
ret.HubsRegistered = 0
|
||||
ret.HubsOperator = 0
|
||||
ret.Slots = 0
|
||||
|
||||
if len(matches[3]) > 1 {
|
||||
ret.Speed = matches[3][:len(matches[3])-2]
|
||||
} else {
|
||||
ret.Speed = ""
|
||||
}
|
||||
ret.Flag = UserFlag(matches[3][len(matches[3])-1])
|
||||
ret.Email = this.Unescape(matches[4])
|
||||
maybeParse(matches[5], &ret.ShareSize, 0)
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
// Couldn't get anything out of it...
|
||||
return nil, errors.New("Malformed MyINFO")
|
||||
}
|
||||
|
||||
// Returns the MyINFO command, WITH leading $MyINFO, and WITHOUT trailing pipe
|
||||
func (this *NmdcProtocol) getUserMyINFO(u *UserInfo) string {
|
||||
return fmt.Sprintf(
|
||||
"$MyINFO $ALL %s %s<%s V:%s,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$",
|
||||
u.Nick,
|
||||
u.Description,
|
||||
u.ClientTag,
|
||||
strings.Replace(u.ClientVersion, ",", "-", -1), // just in case
|
||||
u.ConnectionMode,
|
||||
u.HubsUnregistered,
|
||||
u.HubsRegistered,
|
||||
u.HubsOperator,
|
||||
u.Slots,
|
||||
u.Speed,
|
||||
u.Flag,
|
||||
u.Email,
|
||||
u.ShareSize,
|
||||
)
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) SayKeepalive() error {
|
||||
return this.hc.SayRaw("|")
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) ProtoMessageSeparator() string {
|
||||
return "|"
|
||||
}
|
||||
|
||||
func (this *NmdcProtocol) unlock(lock []byte) string {
|
||||
|
||||
nibble_swap := func(b byte) byte {
|
||||
return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)
|
||||
}
|
||||
|
||||
chr := func(b byte) string {
|
||||
if b == 0 || b == 5 || b == 36 || b == 96 || b == 124 || b == 126 {
|
||||
return fmt.Sprintf("/%%DCN%04d%%/", b)
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
key := chr(nibble_swap(lock[0] ^ lock[len(lock)-2] ^ lock[len(lock)-3] ^ 5))
|
||||
for i := 1; i < len(lock); i += 1 {
|
||||
key += chr(nibble_swap(lock[i] ^ lock[i-1]))
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
17
Protocol.go
Normal file
17
Protocol.go
Normal file
@ -0,0 +1,17 @@
|
||||
package libnmdc
|
||||
|
||||
type Protocol interface {
|
||||
ProcessCommand(msg string)
|
||||
|
||||
Escape(string) string
|
||||
|
||||
Unescape(string) string
|
||||
|
||||
SayPublic(string)
|
||||
|
||||
SayPrivate(user, message string)
|
||||
|
||||
SayKeepalive() error
|
||||
|
||||
ProtoMessageSeparator() string
|
||||
}
|
105
UserInfo.go
105
UserInfo.go
@ -1,13 +1,5 @@
|
||||
package libnmdc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This structure represents a user connected to a hub.
|
||||
type UserInfo struct {
|
||||
Nick string
|
||||
@ -27,19 +19,6 @@ type UserInfo struct {
|
||||
IPAddress string
|
||||
}
|
||||
|
||||
var rx_myinfo *regexp.Regexp
|
||||
var rx_myinfo_notag *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
// Format: $ALL <nick> <description>$ $<connection><flag>$<e-mail>$<sharesize>$
|
||||
|
||||
HEAD := `(?ms)^\$ALL ([^ ]+) `
|
||||
FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$`
|
||||
|
||||
rx_myinfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT)
|
||||
rx_myinfo_notag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag
|
||||
}
|
||||
|
||||
func NewUserInfo(username string) *UserInfo {
|
||||
return &UserInfo{
|
||||
Nick: username,
|
||||
@ -47,87 +26,3 @@ func NewUserInfo(username string) *UserInfo {
|
||||
HubsUnregistered: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func maybeParse(str string, dest *uint64, default_val uint64) {
|
||||
sz, err := strconv.ParseUint(str, 10, 64)
|
||||
if err == nil {
|
||||
*dest = sz
|
||||
} else {
|
||||
*dest = default_val
|
||||
}
|
||||
}
|
||||
|
||||
func (this *UserInfo) fromMyINFO(protomsg string) error {
|
||||
|
||||
// Normal format (with tag in exact V/M/H/S order)
|
||||
matches := rx_myinfo.FindStringSubmatch(protomsg)
|
||||
if matches != nil {
|
||||
this.Nick = matches[1]
|
||||
this.Description = Unescape(matches[2])
|
||||
this.ClientTag = Unescape(matches[3])
|
||||
this.ClientVersion = matches[4]
|
||||
this.ConnectionMode = ConnectionMode(matches[5][0])
|
||||
maybeParse(matches[6], &this.HubsUnregistered, 0)
|
||||
maybeParse(matches[7], &this.HubsRegistered, 0)
|
||||
maybeParse(matches[8], &this.HubsOperator, 0)
|
||||
maybeParse(matches[9], &this.Slots, 0)
|
||||
if len(matches[10]) > 1 {
|
||||
this.Speed = matches[10][:len(matches[10])-2]
|
||||
} else {
|
||||
this.Speed = ""
|
||||
}
|
||||
this.Flag = UserFlag(matches[10][len(matches[10])-1])
|
||||
this.Email = Unescape(matches[11])
|
||||
maybeParse(matches[12], &this.ShareSize, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// No-tag format, used in early connection
|
||||
matches = rx_myinfo_notag.FindStringSubmatch(protomsg)
|
||||
if matches != nil {
|
||||
this.Nick = matches[1]
|
||||
this.Description = Unescape(matches[2])
|
||||
this.ClientTag = ""
|
||||
this.ClientVersion = "0"
|
||||
this.ConnectionMode = CONNECTIONMODE_PASSIVE
|
||||
this.HubsUnregistered = 0
|
||||
this.HubsRegistered = 0
|
||||
this.HubsOperator = 0
|
||||
this.Slots = 0
|
||||
|
||||
if len(matches[3]) > 1 {
|
||||
this.Speed = matches[3][:len(matches[3])-2]
|
||||
} else {
|
||||
this.Speed = ""
|
||||
}
|
||||
this.Flag = UserFlag(matches[3][len(matches[3])-1])
|
||||
this.Email = Unescape(matches[4])
|
||||
maybeParse(matches[5], &this.ShareSize, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Couldn't get anything out of it...
|
||||
return errors.New("Malformed MyINFO")
|
||||
}
|
||||
|
||||
// Returns the MyINFO command, WITH leading $MyINFO, and WITHOUT trailing pipe
|
||||
func (this *UserInfo) toMyINFO() string {
|
||||
return fmt.Sprintf(
|
||||
"$MyINFO $ALL %s %s<%s V:%s,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$",
|
||||
this.Nick,
|
||||
this.Description,
|
||||
this.ClientTag,
|
||||
strings.Replace(this.ClientVersion, ",", "-", -1), // just in case
|
||||
this.ConnectionMode,
|
||||
this.HubsUnregistered,
|
||||
this.HubsRegistered,
|
||||
this.HubsOperator,
|
||||
this.Slots,
|
||||
this.Speed,
|
||||
this.Flag,
|
||||
this.Email,
|
||||
this.ShareSize,
|
||||
)
|
||||
}
|
||||
|
57
libnmdc.go
57
libnmdc.go
@ -2,9 +2,7 @@ package libnmdc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -17,52 +15,21 @@ const (
|
||||
RECONNECT_IF_NO_DATA_RECIEVED_IN time.Duration = 24 * time.Hour // we expect keepalives wayyyy more frequently than this
|
||||
)
|
||||
|
||||
var rx_protocolMessage *regexp.Regexp
|
||||
var rx_publicChat *regexp.Regexp
|
||||
var rx_incomingTo *regexp.Regexp
|
||||
var rx_userCommand *regexp.Regexp
|
||||
var ErrNotConnected error = errors.New("Not connected")
|
||||
|
||||
func init() {
|
||||
// With the `m` flag, use \A instead of ^ to anchor to start
|
||||
// This fixes accidentally finding a better match in the middle of a multi-line message
|
||||
type HubProtocol int
|
||||
|
||||
rx_protocolMessage = regexp.MustCompile(`(?ms)\A[^|]*\|`)
|
||||
rx_publicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`)
|
||||
rx_incomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`)
|
||||
rx_userCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`)
|
||||
}
|
||||
const (
|
||||
HubProtocolAutodetect = 0
|
||||
HubProtocolNmdc = 1
|
||||
HubProtocolAdc = 2
|
||||
)
|
||||
|
||||
func Unescape(encoded string) string {
|
||||
v1 := strings.Replace(encoded, "$", "$", -1)
|
||||
v2 := strings.Replace(v1, "|", "|", -1)
|
||||
return strings.Replace(v2, "&", "&", -1)
|
||||
}
|
||||
|
||||
func Escape(plaintext string) string {
|
||||
v1 := strings.Replace(plaintext, "&", "&", -1)
|
||||
v2 := strings.Replace(v1, "|", "|", -1)
|
||||
return strings.Replace(v2, "$", "$", -1)
|
||||
}
|
||||
|
||||
func unlock(lock []byte) string {
|
||||
|
||||
nibble_swap := func(b byte) byte {
|
||||
return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F)
|
||||
}
|
||||
|
||||
chr := func(b byte) string {
|
||||
if b == 0 || b == 5 || b == 36 || b == 96 || b == 124 || b == 126 {
|
||||
return fmt.Sprintf("/%%DCN%04d%%/", b)
|
||||
func maybeParse(str string, dest *uint64, default_val uint64) {
|
||||
sz, err := strconv.ParseUint(str, 10, 64)
|
||||
if err == nil {
|
||||
*dest = sz
|
||||
} else {
|
||||
return string(b)
|
||||
*dest = default_val
|
||||
}
|
||||
}
|
||||
|
||||
key := chr(nibble_swap(lock[0] ^ lock[len(lock)-2] ^ lock[len(lock)-3] ^ 5))
|
||||
for i := 1; i < len(lock); i += 1 {
|
||||
key += chr(nibble_swap(lock[i] ^ lock[i-1]))
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user