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 $ $$$$ 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 this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE}) 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_BAD_LOGIN_FAILURE}) this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."}) case "$BadPass": this.hc.processEvent(HubEvent{EventType: EVENT_BAD_LOGIN_FAILURE}) 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_BAD_LOGIN_FAILURE}) 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|| 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) error { return this.hc.SayRaw("<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|") } func (this *NmdcProtocol) SayPrivate(recipient, message string) error { return this.hc.SayRaw("$To: " + recipient + " From: " + this.hc.Hco.Self.Nick + " $<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|") } func (this *NmdcProtocol) SayInfo() error { return 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 }