func main() { flag.Parse() oldState, err := terminal.MakeRaw(0) if err != nil { panic(err.Error()) } defer terminal.Restore(0, oldState) term := terminal.NewTerminal(os.Stdin, "> ") if width, height, err := terminal.GetSize(0); err == nil { term.SetSize(width, height) } if len(*configFile) == 0 { homeDir := os.Getenv("HOME") if len(homeDir) == 0 { homeDir = "/" } *configFile = homeDir + "/.xmpp-client" } config, err := ParseConfig(*configFile) if err != nil { alert(term, "Failed to parse config file: "+err.Error()) config = new(Config) if !enroll(config, term) { return } config.filename = *configFile config.Save() } password := config.Password if len(password) == 0 { if password, err = term.ReadPassword(fmt.Sprintf("Password for %s (will not be saved to disk): ", config.Account)); err != nil { alert(term, "Failed to read password: "******"@", 2) if len(parts) != 2 { alert(term, "invalid username (want user@domain): "+config.Account) return } user := parts[0] domain := parts[1] var addr string if len(config.Server) > 0 && config.Port > 0 { addr = fmt.Sprintf("%s:%d", config.Server, config.Port) } else { if len(config.Proxies) > 0 { alert(term, "Cannot connect via a proxy without Server and Port being set in the config file as an SRV lookup would leak information.") return } host, port, err := xmpp.Resolve(domain) if err != nil { alert(term, "Failed to resolve XMPP server: "+err.Error()) return } addr = fmt.Sprintf("%s:%d", host, port) } var dialer proxy.Dialer for i := len(config.Proxies) - 1; i >= 0; i-- { u, err := url.Parse(config.Proxies[i]) if err != nil { alert(term, "Failed to parse "+config.Proxies[i]+" as a URL: "+err.Error()) return } if dialer == nil { dialer = proxy.Direct } if dialer, err = proxy.FromURL(u, dialer); err != nil { alert(term, "Failed to parse "+config.Proxies[i]+" as a proxy: "+err.Error()) return } } xmppConfig := &xmpp.Config{ Log: &lineLogger{term, nil}, Create: *createAccount, } if len(config.RawLogFile) > 0 { rawLog, err := os.OpenFile(config.RawLogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { alert(term, "Failed to open raw log file: "+err.Error()) return } lock := new(sync.Mutex) in := rawLogger{ out: rawLog, prefix: []byte("<- "), lock: lock, } out := rawLogger{ out: rawLog, prefix: []byte("-> "), lock: lock, } in.other, out.other = &out, &in xmppConfig.InLog = &in xmppConfig.OutLog = &out defer in.flush() defer out.flush() } if dialer != nil { info(term, "Making connection to "+addr+" via proxy") if xmppConfig.Conn, err = dialer.Dial("tcp", addr); err != nil { alert(term, "Failed to connect via proxy: "+err.Error()) return } } conn, err := xmpp.Dial(addr, user, domain, password, xmppConfig) if err != nil { alert(term, "Failed to connect to XMPP server: "+err.Error()) return } s := Session{ account: config.Account, conn: conn, term: term, conversations: make(map[string]*otr.Conversation), knownStates: make(map[string]string), privateKey: new(otr.PrivateKey), config: config, pendingRosterChan: make(chan *rosterEdit), pendingSubscribes: make(map[string]string), lastActionTime: time.Now(), } info(term, "Fetching roster") //var rosterReply chan xmpp.Stanza rosterReply, _, err := s.conn.RequestRoster() if err != nil { alert(term, "Failed to request roster: "+err.Error()) return } conn.SignalPresence("") s.input = Input{ term: term, uidComplete: new(priorityList), } commandChan := make(chan interface{}) go s.input.ProcessCommands(commandChan) stanzaChan := make(chan xmpp.Stanza) go s.readMessages(stanzaChan) s.privateKey.Parse(config.PrivateKey) s.timeouts = make(map[xmpp.Cookie]time.Time) ticker := time.NewTicker(1 * time.Second) MainLoop: for { select { case now := <-ticker.C: haveExpired := false for _, expiry := range s.timeouts { if now.After(expiry) { haveExpired = true break } } if !haveExpired { continue } newTimeouts := make(map[xmpp.Cookie]time.Time) for cookie, expiry := range s.timeouts { if now.After(expiry) { s.conn.Cancel(cookie) } else { newTimeouts[cookie] = expiry } } s.timeouts = newTimeouts case edit := <-s.pendingRosterChan: if !edit.isComplete { info(s.term, "Please edit "+edit.fileName+" and run /rostereditdone when complete") s.pendingRosterEdit = edit continue } if s.processEditedRoster(edit) { s.pendingRosterEdit = nil } else { alert(s.term, "Please reedit file and run /rostereditdone again") } case rosterStanza, ok := <-rosterReply: if !ok { alert(s.term, "Failed to read roster: "+err.Error()) return } if s.roster, err = xmpp.ParseRoster(rosterStanza); err != nil { alert(s.term, "Failed to parse roster: "+err.Error()) return } for _, entry := range s.roster { s.input.AddUser(entry.Jid) } info(s.term, "Roster received") case cmd, ok := <-commandChan: if !ok { warn(term, "Exiting because command channel closed") break MainLoop } s.lastActionTime = time.Now() switch cmd := cmd.(type) { case quitCommand: break MainLoop case versionCommand: replyChan, cookie, err := s.conn.SendIQ(cmd.User, "get", xmpp.VersionQuery{}) if err != nil { alert(s.term, "Error sending version request: "+err.Error()) continue } s.timeouts[cookie] = time.Now().Add(5 * time.Second) go s.awaitVersionReply(replyChan, cmd.User) case rosterCommand: info(s.term, "Current roster:") maxLen := 0 for _, item := range s.roster { if maxLen < len(item.Jid) { maxLen = len(item.Jid) } } for _, item := range s.roster { line := " " line += item.Jid numSpaces := 1 + (maxLen - len(item.Jid)) for i := 0; i < numSpaces; i++ { line += " " } line += item.Subscription + "\t" + item.Name if state, ok := s.knownStates[item.Jid]; ok { line += "\t" + state } info(s.term, line) } case rosterEditCommand: if s.pendingRosterEdit != nil { warn(s.term, "Aborting previous roster edit") s.pendingRosterEdit = nil } rosterCopy := make([]xmpp.RosterEntry, len(s.roster)) copy(rosterCopy, s.roster) go s.editRoster(rosterCopy) case rosterEditDoneCommand: if s.pendingRosterEdit == nil { warn(s.term, "No roster edit in progress. Use /rosteredit to start one") continue } go s.loadEditedRoster(*s.pendingRosterEdit) case confirmCommand: s.handleConfirmOrDeny(cmd.User, true /* confirm */) case denyCommand: s.handleConfirmOrDeny(cmd.User, false /* deny */) case addCommand: s.conn.SendPresence(cmd.User, "subscribe", "" /* generate id */) case msgCommand: conversation, ok := s.conversations[cmd.to] var msgs [][]byte if ok { var err error msgs, err = conversation.Send([]byte(cmd.msg)) if err != nil { alert(s.term, err.Error()) break } } else { msgs = [][]byte{[]byte(cmd.msg)} } for _, msg := range msgs { s.conn.Send(cmd.to, string(msg)) } case otrCommand: s.conn.Send(string(cmd.User), string(otr.QueryMessage)) case authQACommand: to := string(cmd.User) conversation, ok := s.conversations[to] if !ok { alert(s.term, "Can't authenticate without a secure conversation established") break } msgs, err := conversation.Authenticate(cmd.Question, []byte(cmd.Secret)) if err != nil { alert(s.term, "Error while starting authentication with "+to+": "+err.Error()) } for _, msg := range msgs { s.conn.Send(to, string(msg)) } } case rawStanza, ok := <-stanzaChan: if !ok { warn(term, "Exiting because channel to server closed") break MainLoop } switch stanza := rawStanza.Value.(type) { case *xmpp.ClientMessage: s.processClientMessage(stanza) case *xmpp.ClientPresence: s.processPresence(stanza) case *xmpp.ClientIQ: if stanza.Type != "get" && stanza.Type != "set" { continue } reply := s.processIQ(stanza) if reply == nil { reply = xmpp.ErrorReply{ Type: "cancel", Error: xmpp.ErrorBadRequest{}, } } if err := s.conn.SendIQReply(stanza.From, "result", stanza.Id, reply); err != nil { alert(term, "Failed to send IQ message: "+err.Error()) } default: info(term, fmt.Sprintf("%s %s", rawStanza.Name, rawStanza.Value)) } } } os.Stdout.Write([]byte("\n")) }
func enroll(config *Config, term *terminal.Terminal) bool { var err error warn(term, "Enrolling new config file") var domain string for { term.SetPrompt("Account (i.e. [email protected], enter to quit): ") if config.Account, err = term.ReadLine(); err != nil || len(config.Account) == 0 { return false } parts := strings.SplitN(config.Account, "@", 2) if len(parts) != 2 { alert(term, "invalid username (want user@domain): "+config.Account) continue } domain = parts[1] break } var proxyStr string term.SetPrompt("Proxy (i.e socks5://127.0.0.1:9050, enter for none): ") for { if proxyStr, err = term.ReadLine(); err != nil { return false } if len(proxyStr) == 0 { break } u, err := url.Parse(proxyStr) if err != nil { alert(term, "Failed to parse "+proxyStr+" as a URL: "+err.Error()) continue } if _, err = proxy.FromURL(u, proxy.Direct); err != nil { alert(term, "Failed to parse "+proxyStr+" as a proxy: "+err.Error()) continue } break } if len(proxyStr) > 0 { config.Proxies = []string{proxyStr} info(term, "Since you selected a proxy, we need to know the server and port to connect to as a SRV lookup would leak information every time.") term.SetPrompt("Server (i.e. xmpp.example.com, enter to lookup using unproxied DNS): ") if config.Server, err = term.ReadLine(); err != nil { return false } if len(config.Server) == 0 { var port uint16 info(term, "Performing SRV lookup") if config.Server, port, err = xmpp.Resolve(domain); err != nil { alert(term, "SRV lookup failed: "+err.Error()) return false } config.Port = int(port) info(term, "Resolved "+config.Server+":"+strconv.Itoa(config.Port)) } else { for { term.SetPrompt("Port (enter for 5222): ") portStr, err := term.ReadLine() if err != nil { return false } if len(portStr) == 0 { portStr = "5222" } if config.Port, err = strconv.Atoi(portStr); err != nil || config.Port <= 0 || config.Port > 65535 { info(term, "Port numbers must be 0 < port <= 65535") continue } break } } } term.SetPrompt("File to import libotr private key from (enter to generate): ") var priv otr.PrivateKey for { importFile, err := term.ReadLine() if err != nil { return false } if len(importFile) > 0 { privKeyBytes, err := ioutil.ReadFile(importFile) if err != nil { alert(term, "Failed to open private key file: "+err.Error()) continue } if !priv.Import(privKeyBytes) { alert(term, "Failed to parse libotr private key file (the parser is pretty simple I'm afraid)") continue } break } else { info(term, "Generating private key...") priv.Generate(rand.Reader) break } } config.PrivateKey = priv.Serialize(nil) term.SetPrompt("> ") return true }