diff --git a/HubAddress.go b/HubAddress.go index b282f43..12d5c61 100644 --- a/HubAddress.go +++ b/HubAddress.go @@ -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 + } +} diff --git a/HubConnection.go b/HubConnection.go index 2137666..6ee8597 100644 --- a/HubConnection.go +++ b/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 diff --git a/NmdcProtocol.go b/NmdcProtocol.go new file mode 100644 index 0000000..1ac3fd1 --- /dev/null +++ b/NmdcProtocol.go @@ -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 $ $$$$ + + 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 +} diff --git a/Protocol.go b/Protocol.go new file mode 100644 index 0000000..fe95707 --- /dev/null +++ b/Protocol.go @@ -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 +} diff --git a/UserInfo.go b/UserInfo.go index 15e3e66..cc51f44 100644 --- a/UserInfo.go +++ b/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 $ $$$$ - - 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, - ) -} diff --git a/libnmdc.go b/libnmdc.go index e847e75..972f63f 100644 --- a/libnmdc.go +++ b/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) +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 } - - 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 }