// libnmdc project libnmdc.go package libnmdc import ( "crypto/tls" "errors" "fmt" "net" "regexp" "strings" "time" ) const ( DEFAULT_CLIENT_TAG string = "libnmdc.go" DEFAULT_HUB_NAME string = "(unknown)" SEND_KEEPALIVE_EVERY time.Duration = 29 * time.Second ) type ConnectionState int 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 var ErrNotConnected error = errors.New("Not connected") func init() { rx_protocolMessage = regexp.MustCompile("(?ms)^[^|]*\\|") rx_publicChat = regexp.MustCompile("(?ms)^<([^>]*)> (.*)$") rx_incomingTo = regexp.MustCompile("(?ms)^([^ ]+) From: ([^ ]+) \\$<([^>]*)> (.*)") } type HubConnectionOptions struct { Address HubAddress SkipVerifyTLS bool // using a negative verb, because bools default to false Self UserInfo NickPassword string // Returning messages in async mode NumEventsToBuffer uint // Returning messages in sync mode OnEventSync func(HubEvent) } type HubConnection struct { // Supplied parameters Hco *HubConnectionOptions // Current remote status HubName string State ConnectionState Users map[string]UserInfo // Streamed events processEvent func(HubEvent) OnEvent chan HubEvent // Private state conn net.Conn // this is an interface connValid bool 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.processEvent(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.processEvent(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 { if this.connValid { _, err := this.conn.Write([]byte(protocolCommand)) return err } else { return ErrNotConnected } } 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.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: NMDCUnescape(pubchat_parts[2])}) return } // System messages // ``````````````` if message[0] != '$' { this.processEvent(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.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": this.SayRaw("$MyPass " + NMDCEscape(this.Hco.NickPassword) + "|") case "$Quit": delete(this.Users, commandParts[1]) this.processEvent(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.processEvent(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:": 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: txparts[4]}) valid = true } } if !valid { this.processEvent(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.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED}) this.State = CONNECTIONSTATE_CONNECTED } case "$ForceMove": this.Hco.Address = HubAddress(commandParts[1]) this.conn.Close() // we'll reconnect onto the new address // IGNORABLE COMMANDS case "$Supports": case "$UserCommand": // TODO $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new|| case "$UserList": case "$OpList": case "$HubTopic": case "$Search": case "$ConnectToMe": default: this.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"}) } } func CheckIsNetTimeout(err error) bool { if err == nil { return false } switch err.(type) { case net.Error: return err.(net.Error).Timeout() default: return false } } func (this *HubConnection) worker() { var fullBuffer string var err error = nil var nbytes int = 0 for { // If we're not connected, attempt reconnect if this.conn == nil { fullBuffer = "" // clear if this.Hco.Address.IsSecure() { this.conn, err = tls.Dial("tcp", this.Hco.Address.GetHostOnly(), &tls.Config{ InsecureSkipVerify: this.Hco.SkipVerifyTLS, }) } else { this.conn, err = net.Dial("tcp", this.Hco.Address.GetHostOnly()) } if err != nil { this.State = CONNECTIONSTATE_DISCONNECTED this.connValid = false } else { this.State = CONNECTIONSTATE_CONNECTING this.connValid = true this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTING}) } } // Read from socket into our local buffer (blocking) if this.connValid { readBuff := make([]byte, 1024) this.conn.SetReadDeadline(time.Now().Add(SEND_KEEPALIVE_EVERY)) nbytes, err = this.conn.Read(readBuff) if CheckIsNetTimeout(err) { // No data before read deadline err = nil // Send KA packet _, err = this.conn.Write([]byte("|")) } if nbytes > 0 { fullBuffer += string(readBuff[0:nbytes]) } } // 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(protocolMessage[:len(protocolMessage)-1]) fullBuffer = fullBuffer[len(protocolMessage):] } else { break } } // Maybe we disconnected // Perform this check *last*, to ensure we've had a final shot at // clearing out any queued messages if err != nil { this.State = CONNECTIONSTATE_DISCONNECTED this.conn = nil this.connValid = false this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_DISCONNECTED, Message: err.Error()}) time.Sleep(30 * time.Second) // Wait before reconnect continue } } } func (this *HubConnectionOptions) prepareConnection() *HubConnection { if this.Self.ClientTag == "" { this.Self.ClientTag = DEFAULT_CLIENT_TAG } hc := HubConnection{ Hco: this, HubName: DEFAULT_HUB_NAME, State: CONNECTIONSTATE_DISCONNECTED, Users: make(map[string]UserInfo), } return &hc } // 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.NumEventsToBuffer < 1 { this.NumEventsToBuffer = 1 } hc := this.prepareConnection() hc.OnEvent = make(chan HubEvent, this.NumEventsToBuffer) hc.processEvent = func(ev HubEvent) { hc.OnEvent <- ev } go hc.worker() return hc } // Connects to an NMDC server, and blocks forever to handle protocol messages. // Client code should supply an event handling function. func (this *HubConnectionOptions) ConnectSync() { hc := this.prepareConnection() hc.worker() }