package libnmdc import ( "encoding/base32" "fmt" "regexp" "strconv" "strings" ) type adcState int const ( adcStateProtocol adcState = 0 adcStateIdentify adcState = 1 adcStateVerify adcState = 2 adcStateNormal adcState = 3 adcStateData adcState = 4 ) type AdcProtocol struct { hc *HubConnection state adcState sid, pid, cid string // all in base32 encoding supports map[string]struct{} } const ( // extra extensions that aren't flagged in SUPPORTS adcSeparateApVe string = "SEPARATE_AP_VE" // we invented this string ) func NewAdcProtocol(hc *HubConnection) Protocol { proto := AdcProtocol{ hc: hc, state: adcStateProtocol, supports: make(map[string]struct{}), } rxPid := regexp.MustCompile("^[A-Z2-7]{39}$") if !rxPid.MatchString(hc.Hco.AdcPID) { hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Invalid custom PID, regenerating"}) hc.Hco.AdcPID = NewPID() } pid_base32 := hc.Hco.AdcPID cid_base32, err := proto.pid2cid(pid_base32) if err != nil { panic(err) } proto.cid = cid_base32 proto.pid = pid_base32 // Start logging in hc.SayRaw("HSUP ADBASE ADTIGR ADUCMD\n") return &proto } func (this *AdcProtocol) pid2cid(pid_base32 string) (string, error) { pid_raw, err := base32.StdEncoding.DecodeString(pid_base32 + "=") if err != nil { return "", err } cid_raw := Tiger(string(pid_raw)) cid_base32 := Base32(cid_raw) return cid_base32, nil } func (this *AdcProtocol) SID2Nick(sid string) (string, bool) { this.hc.usersMut.Lock() defer this.hc.usersMut.Unlock() nick, ok := this.hc.userSIDs[sid] return nick, ok } func (this *AdcProtocol) Nick2SID(targetNick string) (string, bool) { this.hc.usersMut.Lock() defer this.hc.usersMut.Unlock() for sid, nick := range this.hc.userSIDs { if nick == targetNick { return sid, true } } return "", false } func (this *AdcProtocol) ProcessCommand(msg string) { if len(msg) == 0 { return } this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: msg}) parts := strings.Split(msg, " ") switch parts[0] { case "ISUP": if !(this.state == adcStateProtocol || this.state == adcStateNormal) { this.malformed(parts) return } for _, supportflag := range parts[1:] { if len(supportflag) < 2 { this.malformed(parts) return } if supportflag[0:2] == "AD" { this.supports[supportflag[2:]] = struct{}{} } else if supportflag[0:2] == "RM" { delete(this.supports, supportflag[2:]) } else { this.malformed(parts) return } } if this.state == adcStateProtocol { this.state = adcStateIdentify } case "ISID": if this.state != adcStateIdentify { this.malformed(parts) return } this.sid = parts[1] // State transition IDENTIFY --> VERIFY and send our own info this.SayInfo() this.state = adcStateVerify case "IINF": // Hub telling information about itself // ADCH++ sends this once we are successfully logged in flags, err := this.parts2flags(parts[1:]) if err != nil { this.logError(err) return } if flags["CT"] != "32" { this.malformed(parts) return } err = this.handleHubInfo(flags) if err != nil { this.logError(err) return } if this.state != adcStateNormal { this.enterNormalState() // successful login } case "BINF": if this.state != adcStateNormal { this.enterNormalState() // successful login } sid := parts[1] flags, err := this.parts2flags(parts[2:]) if err != nil { this.logError(err) return } // Log this user in, and associate this SID with this user this.hc.usersMut.Lock() defer this.hc.usersMut.Unlock() oldNick, sidExists := this.hc.userSIDs[sid] uinfo := UserInfo{} if sidExists { uinfo_lookup, ok := this.hc.users[oldNick] if !ok { // Shouldn't happen this.hc.processEvent(HubEvent{ EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: fmt.Sprintf("Hub connection corrupted (missing info for SID='%s' nick='%s'), disconnecting", sid, oldNick), }) this.hc.Disconnect() return } uinfo = uinfo_lookup } this.updateUserInfo(&uinfo, flags) newNick := uinfo.Nick if len(newNick) == 0 { this.logError(fmt.Errorf("Zero-length nick for user (SID='%s')", sid)) } shouldHandleNewUser := false if sidExists && oldNick != newNick { // Nick change = delete all trace of this user first, treat as new delete(this.hc.users, oldNick) delete(this.hc.userSIDs, sid) this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: oldNick}) shouldHandleNewUser = true } else if sidExists && oldNick == newNick { // Updating existing user this.hc.users[newNick] = uinfo this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: newNick}) } else if !sidExists { // User joined shouldHandleNewUser = true } // if shouldHandleNewUser { // Install this SID as pointing to this nick this.hc.userSIDs[sid] = uinfo.Nick // Check if this nick was in use by any other SID already for otherSid, otherSidNick := range this.hc.userSIDs { if otherSidNick == newNick && otherSid != sid { this.hc.processEvent(HubEvent{ EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: fmt.Sprintf("Hub connection corrupted (duplicate SIDs '%s' and '%s' for nick '%s'), disconnecting", sid, otherSid, newNick), }) this.hc.Disconnect() return } } // Notifications this.hc.users[newNick] = uinfo this.hc.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: newNick}) } case "IMSG": // General message from the hub if len(parts) < 2 { this.malformed(parts) return } this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.unescape(parts[1])}) case "ISTA": // Error message from the hub if len(parts) < 3 { this.malformed(parts) return } code, _ := strconv.Atoi(parts[1]) msg := this.unescape(parts[2]) this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.ErrorMessage(code, msg)}) case "IQUI": // Error message from the hub // IQUI V3M6 DI1 MSNick\staken,\splease\spick\sanother\sone TL-1 if len(parts) < 2 { this.malformed(parts) return } sid := parts[1] flags, err := this.parts2flags(parts[2:]) if err != nil { return } if sid == this.sid { if msg, ok := flags["MS"]; ok { this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection because: " + this.unescape(msg)}) } else { this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection"}) } } else { this.hc.usersMut.Lock() defer this.hc.usersMut.Unlock() otherSidNick, ok := this.hc.userSIDs[sid] if ok { delete(this.hc.userSIDs, sid) delete(this.hc.users, otherSidNick) this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: otherSidNick}) } else { // ?? this.logError(fmt.Errorf("An unknown user quit the hub (SID=%s)", sid)) } } case "BMSG": // Message from a user // BMSG ZVF4 hi if len(parts) < 3 { this.malformed(parts) return } sid := this.unescape(parts[1]) msg := this.unescape(parts[2]) this.hc.usersMut.Lock() defer this.hc.usersMut.Unlock() nick, ok := this.hc.userSIDs[sid] if !ok { this.logError(fmt.Errorf("Recieved message from unknown SID '%s'", sid)) return } this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: nick, Message: msg}) case "IGPA": // Password is needed // IGPA 7EIAAAECLMAAAPJQAAADQQYAAAWAYAAAKVFQAAF6EAAAAAYFAAAA // HPAS LZDIJOTZDPWHINHGPT5RHT6WLU7DRME7DQO2O3Q if len(parts) < 2 { this.malformed(parts) return } /* For GPA/PAS, assuming that '12345' is the random data supplied in GPA, then; PAS = Base32( Hash( password + '12345' ) ) GPA: The data parameter is at least 24 random bytes (base32 encoded). */ data_base32 := parts[1] if len(data_base32)%8 != 0 { data_base32 += strings.Repeat("=", 8-(len(data_base32)%8)) } data_raw, err := base32.StdEncoding.DecodeString(data_base32) if err != nil { this.logError(err) return } resp := Base32(Tiger(this.hc.Hco.NickPassword + string(data_raw))) this.hc.SayRaw("HPAS " + resp + "\n") case "EMSG": // Private message from other user // EMSG I5RO FMWH test\spm PMI5RO // EMSG sender recip==us message [flags...] if len(parts) < 4 { this.malformed(parts) return } if parts[2] != this.sid { this.logError(fmt.Errorf("Recieved a PM intended for someone else (got SID=%s expected SID=%s)", parts[2], this.sid)) return } senderSid := parts[1] senderNick, ok := this.SID2Nick(parts[1]) if !ok { this.logError(fmt.Errorf("Recieved a PM from an unknown user (SID=%s)", senderSid)) return } msg := this.unescape(parts[3]) this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: senderNick, Message: msg}) case "ICMD": // Usercommand // ICMD ADCH++/About\sthis\shub TTHMSG\s+about\n CT3 if len(parts) < 2 { this.malformed(parts) return } uc := UserCommand{ Message: this.unescape(parts[1]), Type: USERCOMMAND_TYPE_RAW, // default } flags, err := this.parts2flags(parts[2:]) if err != nil { this.malformed(parts) return } if ct, ok := flags["CT"]; ok { ct64, _ := strconv.ParseUint(ct, 10, 64) uc.Context = UserCommandContext(ct64) } if tt, ok := flags["TT"]; ok { uc.Command = tt } if sp, ok := flags["SP"]; ok && sp == "1" { uc.Type = USERCOMMAND_TYPE_SEPARATOR } if co, ok := flags["CO"]; ok && co == "1" { uc.Type = USERCOMMAND_TYPE_NICKLIMITED // "Constrained" in ADC parlance } if rm, ok := flags["RM"]; ok && rm == "1" { uc.RemoveThis = true } this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uc}) // Ignored messages // ```````````````` case "DCTM": // Client-client ConnectToMe case "BSCH": // Search default: this.malformed(parts) } } func (this *AdcProtocol) infoFlagsFor(u *UserInfo) map[string]string { parts := map[string]string{ "NI": u.Nick, "SS": fmt.Sprintf("%d", u.ShareSize), "SF": fmt.Sprintf("%d", u.SharedFiles), "US": fmt.Sprintf("%d", u.UploadSpeedBps), "DS": fmt.Sprintf("%d", u.DownloadSpeedBps), "SL": fmt.Sprintf("%d", u.Slots), "HN": fmt.Sprintf("%d", u.HubsUnregistered), "HR": fmt.Sprintf("%d", u.HubsRegistered), "HO": fmt.Sprintf("%d", u.HubsOperator), } if _, ok := this.supports[adcSeparateApVe]; ok { parts["AP"] = u.ClientTag parts["VE"] = u.ClientVersion } else { parts["VE"] = fmt.Sprintf("%s %s", u.ClientTag, u.ClientVersion) } // Do not send the hub a CT (it decides what type we are) return parts } func (this *AdcProtocol) ourINFO(includePid bool) string { parts := this.infoFlagsFor(this.hc.Hco.Self) parts["ID"] = this.cid if includePid { parts["PD"] = this.pid } ret := "" for k, v := range parts { ret += " " + k + this.escape(v) } return ret[1:] } func (this *AdcProtocol) SayInfo() error { return this.hc.SayRaw("BINF " + this.escape(this.sid) + " " + this.ourINFO(true) + "\n") } func (this *AdcProtocol) parts2flags(parts []string) (map[string]string, error) { flags := make(map[string]string, len(parts)) for _, flag := range parts { if len(flag) < 2 { return nil, fmt.Errorf("Malformed flag '%s'", flag) } flags[flag[0:2]] = this.unescape(flag[2:]) } return flags, nil } func (this *AdcProtocol) handleHubInfo(flags map[string]string) error { if flags["CT"] != "32" { return fmt.Errorf("Expected CT==32") } // IINF DEADCH++\sTest\shub VE2.12.1\s(r"[unknown]")\sRelease HI1 NIADCH++ APADCH++ CT32 // AP: extension 3.24 "Application and version separation in INF" // HI: // Hub properties updated // Special SUPPORT that is only indicated in IINF if _, ok := flags["AP"]; ok { this.supports[adcSeparateApVe] = struct{}{} } // Hub's name is in "NI", hub description in "DE" hubName, ok := flags["NI"] if ok { if hubDesc, ok := flags["DE"]; ok && len(hubDesc) > 0 { hubName += " - " + hubDesc } this.hc.HubName = hubName this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: this.hc.HubName}) } return nil } func (this *AdcProtocol) updateUserInfo(u *UserInfo, flags map[string]string) { // User MyINFO // BINF GUPR IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY PDZMIFLG5EKZG3BDRRMIJPG7ARNA6KW3JVIH3DF7Q NIivysaur5 SL3 FS3 SS0 SF0 HN1 HR0 HO0 VEEiskaltDC++\s2.2.9 US2621440 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA I40.0.0.0 U418301 // BINF GUPR I4172.17.0.1 U418301 IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY VEEiskaltDC++\s2.2.9 SF0 NIivysaur5 SL3 HN1 HO0 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA HR0 FS3 SS0 US2621440 SUSEGA,ADC0,TCP4,UDP4 // Or maybe only incremental: // BINF Z3BA HO1 // TODO for prop, val := range flags { switch prop { case "ID": u.CID = val case "PD": // ignore PID - it will only appear if we're talking about our own user case "NI": u.Nick = val case "SL": u.Slots, _ = strconv.ParseUint(val, 10, 64) case "SS": u.ShareSize, _ = strconv.ParseUint(val, 10, 64) case "SF": u.SharedFiles, _ = strconv.ParseUint(val, 10, 64) case "HN": u.HubsUnregistered, _ = strconv.ParseUint(val, 10, 64) case "HR": u.HubsRegistered, _ = strconv.ParseUint(val, 10, 64) case "HO": u.HubsOperator, _ = strconv.ParseUint(val, 10, 64) case "US": u.UploadSpeedBps, _ = strconv.ParseUint(val, 10, 64) case "DS": u.DownloadSpeedBps, _ = strconv.ParseUint(val, 10, 64) case "KP": u.Keyprint = val case "I4": u.IPv4Address = val case "I6": u.IPv6Address = val case "U4": u.IPv4UDPPort, _ = strconv.ParseUint(val, 10, 64) case "U6": u.IPv6UDPPort, _ = strconv.ParseUint(val, 10, 64) case "SU": u.SupportFlags = make(map[string]struct{}) for _, supportFlag := range strings.Split(val, ",") { u.SupportFlags[supportFlag] = struct{}{} } } } // VE / AP AP, hasAP := flags["AP"] VE, hasVE := flags["VE"] if hasAP && hasVE { u.ClientTag = AP u.ClientVersion = VE } else if hasAP && !hasVE { u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(AP) } else if !hasAP && hasVE { u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(VE) } } func (this *AdcProtocol) getAPVEFromSingle(term string) (string, string) { words := strings.Split(term, " ") if len(words) > 1 { return strings.Join(words[0:len(words)-1], " "), words[len(words)-1] } else { return term, "0" } } func (this *AdcProtocol) enterNormalState() { this.state = adcStateNormal this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED}) this.hc.State = CONNECTIONSTATE_CONNECTED } func (this *AdcProtocol) malformed(parts []string) { this.logError(fmt.Errorf("Ignoring malformed, unhandled, or out-of-state protocol command %v", parts)) } func (this *AdcProtocol) logError(e error) { this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Protocol error: " + e.Error()}) } func (this *AdcProtocol) escape(plaintext string) string { // The string "\s" escapes space, "\n" newline and "\\" backslash. This version of the protocol reserves all other escapes for future use; any message containing unknown escapes must be discarded. v1 := strings.Replace(plaintext, `\`, `\\`, -1) v2 := strings.Replace(v1, "\n", `\n`, -1) return strings.Replace(v2, " ", `\s`, -1) } func (this *AdcProtocol) unescape(encoded string) string { v1 := strings.Replace(encoded, `\s`, " ", -1) v2 := strings.Replace(v1, `\n`, "\n", -1) return strings.Replace(v2, `\\`, `\`, -1) } func (this *AdcProtocol) SayPublic(msg string) error { return this.hc.SayRaw("BMSG " + this.sid + " " + this.escape(msg) + "\n") } func (this *AdcProtocol) SayPrivate(user, message string) error { sid, ok := this.Nick2SID(user) if !ok { return fmt.Errorf("Unknown user '%s'", user) } return this.hc.SayRaw("DMSG " + this.sid + " " + sid + " " + this.escape(message) + "\n") } func (this *AdcProtocol) ProtoMessageSeparator() string { return "\n" } func (this *AdcProtocol) ErrorMessage(code int, msg string) string { severity := code / 100 category := (code % 100) / 10 cat_sub := (code % 100) formatSeverity := func(severity int) string { switch severity { case 0: return "OK" case 1: return "Warning" case 2: return "Error" default: return "" } } formatCategory := func(category int) string { switch category { case 0: return "" case 1: return "Hub not accepting users" case 2: return "Login failed" case 3: return "Access denied" case 4: return "Protocol error" case 5: return "Transfer error" default: return "" } } formatCatSub := func(cat_sub int) string { switch cat_sub { case 11: return "Hub is full" case 12: return "Hub is disabled" case 21: return "Invalid nick" case 22: return "Nick is already in use" case 23: return "Invalid password" case 24: return "CID already connected" case 25: return "Access denied" case 26: return "Registered users only" case 27: return "Invalid PID" case 31: return "Permanently banned" case 32: return "Temporarily banned" default: return "" } } parts := make([]string, 0, 4) if fs := formatSeverity(severity); len(fs) > 0 { parts = append(parts, fs) } if fc := formatCategory(category); len(fc) > 0 { parts = append(parts, fc) } if fcs := formatCatSub(cat_sub); len(fcs) > 0 { parts = append(parts, fcs) } if len(msg) > 0 { parts = append(parts, msg) } return strings.Join(parts, ": ") + fmt.Sprintf(" (code %d)", code) }