commit a8442e902d0e20fba94672c3f52fda78de14bf7e Author: mappu Date: Fri Feb 12 17:03:42 2016 +1300 libnmdc: initial commit diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..75ef545 --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +mode:regex + +\.(?:exe|a)$ +^pkg/ + +^src/(?:github.com|gopkg.in|golang.org)/ diff --git a/src/libnmdc/UserInfo.go b/src/libnmdc/UserInfo.go new file mode 100644 index 0000000..6df15f4 --- /dev/null +++ b/src/libnmdc/UserInfo.go @@ -0,0 +1,152 @@ +// libnmdc project UserInfo.go +package libnmdc + +import ( + "errors" + "fmt" + "regexp" + "strconv" +) + +type UserFlag byte + +const ( + FLAG_NORMAL UserFlag = 1 + FLAG_AWAY_1 UserFlag = 2 + FLAG_AWAY_2 UserFlag = 3 + FLAG_SERVER_1 UserFlag = 4 + FLAG_SERVER_2 UserFlag = 5 + FLAG_SERVER_AWAY_1 UserFlag = 6 + FLAG_SERVER_AWAY_2 UserFlag = 7 + FLAG_FIREBALL_1 UserFlag = 8 + FLAG_FIREBALL_2 UserFlag = 9 + FLAG_FIREBALL_AWAY_1 UserFlag = 10 + FLAG_FIREBALL_AWAY_2 UserFlag = 11 +) + +type ConnectionMode rune + +const ( + CONNECTIONMODE_ACTIVE ConnectionMode = 'A' + CONNECTIONMODE_PASSIVE ConnectionMode = 'P' + CONNECTIONMODE_SOCKS5 ConnectionMode = '5' +) + +// This structure represents a user connected to a hub. +type UserInfo struct { + Nick string + Description string + ClientTag string + Email string + ShareSize uint64 + ConnectionMode ConnectionMode + Flag UserFlag + Slots uint64 + Speed string + HubsUnregistered uint64 + HubsRegistered uint64 + HubsOperator uint64 +} + +var rx_myinfo *regexp.Regexp +var rx_myinfo_notag *regexp.Regexp + +func init() { + // $ALL $ $$$$ + rx_myinfo = regexp.MustCompile("(?ms)^\\$ALL ([^ ]+) ([^<]*)<([^,]*),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>\\$.\\$(.+?)(.)\\$([^$]*)\\$([0-9]*)\\$$") + rx_myinfo_notag = regexp.MustCompile("(?ms)^\\$ALL ([^ ]+) ([^$]*)\\$.\\$(.*)(.)\\$([^$]*)\\$([0-9]*)\\$$") // Fallback for no tag + + /* + sample := "$ALL Betty description$ $0.01\x01$xyz@xyz.com$53054999578$" + sample = "$ALL ivysaur80 $P$10A$$0$" + + u := UserInfo{} + err := u.fromMyINFO(sample) + if err != nil { + fmt.Println(err.Error()) + } else { + fmt.Printf("%+v\n", u) + } + + os.Exit(1) + */ +} + +func NewUserInfo(username string) *UserInfo { + return &UserInfo{ + Nick: username, + ConnectionMode: CONNECTIONMODE_PASSIVE, + 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 M/H/S order) + matches := rx_myinfo.FindStringSubmatch(protomsg) + if matches != nil { + this.Nick = matches[1] + this.Description = NMDCUnescape(matches[2]) + this.ClientTag = NMDCUnescape(matches[3]) + this.ConnectionMode = ConnectionMode(matches[4][0]) + maybeParse(matches[5], &this.HubsUnregistered, 0) + maybeParse(matches[6], &this.HubsRegistered, 0) + maybeParse(matches[7], &this.HubsOperator, 0) + maybeParse(matches[8], &this.Slots, 0) + this.Speed = matches[9] + this.Flag = UserFlag(matches[10][0]) + this.Email = NMDCUnescape(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 = NMDCUnescape(matches[2]) + this.ClientTag = "" + this.ConnectionMode = CONNECTIONMODE_PASSIVE + this.HubsUnregistered = 0 + this.HubsRegistered = 0 + this.HubsOperator = 0 + this.Slots = 0 + this.Speed = matches[3] + this.Flag = UserFlag(matches[4][0]) + this.Email = NMDCUnescape(matches[5]) + maybeParse(matches[6], &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,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$", + this.Nick, + this.Description, + this.ClientTag, + this.ConnectionMode, + this.HubsUnregistered, + this.HubsRegistered, + this.HubsOperator, + this.Slots, + this.Speed, + this.Flag, + this.Email, + this.ShareSize, + ) +} diff --git a/src/libnmdc/libnmdc.go b/src/libnmdc/libnmdc.go new file mode 100644 index 0000000..5c39e8c --- /dev/null +++ b/src/libnmdc/libnmdc.go @@ -0,0 +1,354 @@ +// libnmdc project libnmdc.go +package libnmdc + +import ( + "fmt" + "net" + "regexp" + "strings" + "time" +) + +type ConnectionState int +type HubAddress string +type HubEventType int + +const ( + CONNECTIONSTATE_DISCONNECTED = 1 + CONNECTIONSTATE_CONNECTING = 2 // Handshake in progress + CONNECTIONSTATE_CONNECTED = 3 +) + +const ( + EVENT_PUBLIC = 1 + EVENT_PRIVATE = 2 + EVENT_SYSTEM_MESSAGE_FROM_HUB = 3 + EVENT_SYSTEM_MESSAGE_FROM_CONN = 4 + EVENT_USER_JOINED = 5 + EVENT_USER_PART = 6 + EVENT_USER_UPDATED_INFO = 7 + EVENT_CONNECTION_STATE_CHANGED = 8 + EVENT_HUBNAME_CHANGED = 9 + EVENT_DEBUG_MESSAGE = 10 +) + +var rx_protocolMessage *regexp.Regexp +var rx_publicChat *regexp.Regexp +var rx_incomingTo *regexp.Regexp + +func init() { + rx_protocolMessage = regexp.MustCompile("(?ms)^[^|]*|") + rx_publicChat = regexp.MustCompile("(?ms)^<([^>]*)> (.*)$") + rx_incomingTo = regexp.MustCompile("(?ms)^([^ ]+) From: ([^ ]+) $<([^>]*)> (.*)$") +} + +type HubConnectionOptions struct { + Address HubAddress + Self UserInfo + NickPassword string + NumEventsToBuffer uint +} + +type HubConnection struct { + // Supplied parameters + Hco *HubConnectionOptions + + // Current remote status + HubName string + State ConnectionState + Users map[string]UserInfo + + // Streamed events + OnEvent chan HubEvent + + // Private state + conn net.Conn + sentOurHello bool +} + +type HubEvent struct { + EventType HubEventType + Nick string + Message string + StateChange ConnectionState +} + +func (cs ConnectionState) Format() string { + switch cs { + case CONNECTIONSTATE_DISCONNECTED: + return "Disconnected" + case CONNECTIONSTATE_CONNECTING: + return "Connecting" + case CONNECTIONSTATE_CONNECTED: + return "Connected" + default: + return "?" + } +} + +func NMDCUnescape(encoded string) string { + v1 := strings.Replace(encoded, "$", "$", -1) + v2 := strings.Replace(v1, "|", "|", -1) + return strings.Replace(v2, "&", "&", -1) +} + +func NMDCEscape(plaintext string) string { + v1 := strings.Replace(plaintext, "&", "&", -1) + v2 := strings.Replace(v1, "|", "|", -1) + return strings.Replace(v2, "$", "$", -1) +} + +func (this *HubConnection) SayPublic(message string) { + this.SayRaw("<" + this.Hco.Self.Nick + "> " + NMDCEscape(message) + "|") +} + +func (this *HubConnection) SayPrivate(recipient string, message string) { + this.SayRaw("$To: " + recipient + " From: " + this.Hco.Self.Nick + " $<" + this.Hco.Self.Nick + "> " + NMDCEscape(message) + "|") +} + +func (this *HubConnection) SayInfo() { + this.SayRaw(this.Hco.Self.toMyINFO() + "|") +} + +func (this *HubConnection) userJoined_NameOnly(nick string) { + _, already_existed := this.Users[nick] + if !already_existed { + this.Users[nick] = *NewUserInfo(nick) + this.OnEvent <- HubEvent{EventType: EVENT_USER_JOINED, Nick: nick} + } +} + +func (this *HubConnection) userJoined_Full(uinf *UserInfo) { + _, already_existed := this.Users[uinf.Nick] + if !already_existed { + this.Users[uinf.Nick] = *uinf + this.OnEvent <- HubEvent{EventType: EVENT_USER_JOINED, Nick: uinf.Nick} + } +} + +// Note that protocol messages are transmitted on the caller thread, not from +// any internal libnmdc thread. +func (this *HubConnection) SayRaw(protocolCommand string) error { + _, err := this.conn.Write([]byte(protocolCommand)) + return err +} + +func parseLock(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 +} + +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.OnEvent <- HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: NMDCUnescape(pubchat_parts[2])} + return + } + + // System messages + // ``````````````` + if message[0] != '$' { + this.OnEvent <- HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.HubName, Message: NMDCUnescape(message)} + return + } + + // Protocol messages + // ````````````````` + + commandParts := strings.SplitN(message, " ", 2) + switch commandParts[0] { + + case "$Lock": + this.SayRaw("$Supports NoGetINFO UserCommand UserIP2|" + + "$Key " + parseLock([]byte(commandParts[1])) + "|" + + "$ValidateNick " + NMDCEscape(this.Hco.Self.Nick) + "|") + 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.OnEvent <- HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]} + + case "$ValidateDenide": // sic + if len(this.Hco.NickPassword) > 0 { + this.OnEvent <- HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."} + } else { + this.OnEvent <- HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."} + } + + case "$HubIsFull": + this.OnEvent <- HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."} + + case "$BadPass": + this.OnEvent <- HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."} + + case "$GetPass": + this.SayRaw("$MyPass " + NMDCEscape(this.Hco.NickPassword) + "|") + + case "$Quit": + delete(this.Users, commandParts[1]) + this.OnEvent <- HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]} + + case "$MyINFO": + u := UserInfo{} + err := u.fromMyINFO(commandParts[1]) + if err == nil { + this.userJoined_Full(&u) + } else { + this.OnEvent <- HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()} + } + + case "$NickList": + nicklist := strings.Split(commandParts[1], "$$") + for _, nick := range nicklist { + if len(nick) > 0 { + this.userJoined_NameOnly(nick) + } + } + + case "$To": + if rx_incomingTo.MatchString(commandParts[1]) { + txparts := rx_incomingTo.FindStringSubmatch(commandParts[1]) + if txparts[0] == this.Hco.Self.Nick && txparts[1] == txparts[2] { + this.OnEvent <- HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[1], Message: txparts[3]} + break + } + } + this.OnEvent <- HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"} + + case "$UserIP": + // Final message in PtokaX connection handshake - trigger connection callback. + // This might not be the case for other hubsofts, though + if this.State != CONNECTIONSTATE_CONNECTED { + this.OnEvent <- HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED} + this.State = CONNECTIONSTATE_CONNECTED + } + + case "$UserCommand": + // TODO + + case "$ForceMove": + // TODO + + // IGNORABLE COMMANDS + case "$Supports": + case "$UserList": + case "$OpList": + case "$HubTopic": + case "$Search": + case "$ConnectToMe": + + default: + this.OnEvent <- HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"} + + } +} + +func (this *HubConnection) worker() { + var fullBuffer string + + for { + + // If we're not connected, attempt reconnect + if this.conn == nil { + tmp, err := net.Dial("tcp", string(this.Hco.Address)) + this.conn = tmp + if err == nil { + this.OnEvent <- HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTING} + } + } + + // Read from socket into our local buffer (blocking) + readBuff := make([]byte, 4096) + nbytes, err := this.conn.Read(readBuff) + if nbytes > 0 { + fullBuffer += string(readBuff[0:nbytes]) + } + + // Maybe we disconnected + if err != nil { + this.OnEvent <- HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_DISCONNECTED} + this.conn = nil + time.Sleep(30 * time.Second) // Wait before reconnect + continue + } + + // Attempt to parse a message block + for len(fullBuffer) > 0 { + for len(fullBuffer) > 0 && fullBuffer[0] == '|' { + fullBuffer = fullBuffer[1:] + } + protocolMessage := rx_protocolMessage.FindString(fullBuffer) + if len(protocolMessage) > 0 { + this.processProtocolMessage(string(protocolMessage)) + fullBuffer = fullBuffer[len(protocolMessage):] + } else { + break + } + } + + } + +} + +// Connects to an NMDC server, and spawns a background goroutine to handle +// protocol messages. Client code should select on all the interface channels. +func (this *HubConnectionOptions) Connect() *HubConnection { + + if this.Self.ClientTag == "" { + this.Self.ClientTag = "libnmdc.go" + } + if this.NumEventsToBuffer < 1 { + this.NumEventsToBuffer = 1 + } + + hc := HubConnection{ + Hco: this, + HubName: "(unknown)", + State: CONNECTIONSTATE_DISCONNECTED, + Users: make(map[string]UserInfo), + OnEvent: make(chan HubEvent, this.NumEventsToBuffer), + sentOurHello: false, + } + + go hc.worker() + + return &hc +} diff --git a/src/nmdc/main.go b/src/nmdc/main.go new file mode 100644 index 0000000..8077bba --- /dev/null +++ b/src/nmdc/main.go @@ -0,0 +1,34 @@ +// nmdc project main.go +package main + +import ( + "fmt" + "libnmdc" +) + +func main() { + + opts := libnmdc.HubConnectionOptions{ + Address: "127.0.0.1:411", + Self: libnmdc.UserInfo{Nick: "slowpoke9"}, + } + hub := opts.Connect() + + for { + event := <-hub.OnEvent + switch event.EventType { + case libnmdc.EVENT_CONNECTION_STATE_CHANGED: + fmt.Printf("Connection -- %s\n", event.StateChange.Format()) + + case libnmdc.EVENT_PUBLIC: + fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message) + if event.Message == "how are you" { + hub.SayPublic("good thanks!") + } + + default: + fmt.Printf("%+v\n", event) + + } + } +}