nmdc-telegramfrontend/vendor/code.ivysaur.me/libnmdc/NmdcProtocol.go
2018-06-03 15:28:05 +12:00

408 lines
12 KiB
Go

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
}
func NewNmdcProtocol(hc *HubConnection) Protocol {
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.usersMut.Lock()
delete(this.hc.users, commandParts[1])
this.hc.usersMut.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.usersMut.Lock()
defer this.hc.usersMut.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.usersMut.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.usersMut.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&#124;|
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, "&", "&amp;", -1)
v2 := strings.Replace(v1, "|", "&#124;", -1)
return strings.Replace(v2, "$", "&#36;", -1)
}
func (this *NmdcProtocol) unescape(encoded string) string {
v1 := strings.Replace(encoded, "&#36;", "$", -1)
v2 := strings.Replace(v1, "&#124;", "|", -1)
return strings.Replace(v2, "&amp;", "&", -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) 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
}