func remoteShellHandler(ws *websocket.Conn) { var httpErr *errors.HTTP defer func() { defer ws.Close() if httpErr != nil { var msg string switch httpErr.Code { case http.StatusUnauthorized: msg = "no token provided or session expired, please login again\n" default: msg = httpErr.Message + "\n" } ws.Write([]byte("Error: " + msg)) } }() r := ws.Request() token := context.GetAuthToken(r) if token == nil { httpErr = &errors.HTTP{ Code: http.StatusUnauthorized, Message: "no token provided", } return } appName := r.URL.Query().Get(":appname") a, err := getAppFromContext(appName, r) if err != nil { if herr, ok := err.(*errors.HTTP); ok { httpErr = herr } else { httpErr = &errors.HTTP{ Code: http.StatusInternalServerError, Message: err.Error(), } } return } allowed := permission.Check(token, permission.PermAppRunShell, contextsForApp(&a)...) if !allowed { httpErr = permission.ErrUnauthorized return } buf := &optionalWriterCloser{} var term *terminal.Terminal unitID := r.URL.Query().Get("unit") width, _ := strconv.Atoi(r.URL.Query().Get("width")) height, _ := strconv.Atoi(r.URL.Query().Get("height")) clientTerm := r.URL.Query().Get("term") evt, err := event.New(&event.Opts{ Target: appTarget(appName), Kind: permission.PermAppRunShell, Owner: token, CustomData: event.FormToCustomData(r.Form), Allowed: event.Allowed(permission.PermAppReadEvents, contextsForApp(&a)...), DisableLock: true, }) if err != nil { httpErr = &errors.HTTP{ Code: http.StatusInternalServerError, Message: err.Error(), } return } defer func() { var finalErr error if httpErr != nil { finalErr = httpErr } for term != nil { buf.disableWrite = true var line string line, err = term.ReadLine() if err != nil { break } fmt.Fprintf(evt, "> %s\n", line) } evt.Done(finalErr) }() term = terminal.NewTerminal(buf, "") opts := provision.ShellOptions{ Conn: &cmdLogger{base: ws, term: term}, Width: width, Height: height, Unit: unitID, Term: clientTerm, } err = a.Shell(opts) if err != nil { httpErr = &errors.HTTP{ Code: http.StatusInternalServerError, Message: err.Error(), } } }
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 } term.SetPrompt("Enable debug logging to /tmp/xmpp-client-debug.log? ") if debugLog, err := term.ReadLine(); err != nil || debugLog != "yes" { info(term, "Not enabling debug logging...") } else { info(term, "Debug logging enabled...") config.RawLogFile = "/tmp/xmpp-client-debug.log" } term.SetPrompt("Use Tor?: ") if useTorQuery, err := term.ReadLine(); err != nil || len(useTorQuery) == 0 || useTorQuery[0] != 'y' && useTorQuery[0] != 'Y' { info(term, "Not using Tor...") config.UseTor = false } else { info(term, "Using Tor...") config.UseTor = true } 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) config.OTRAutoAppendTag = true config.OTRAutoStartSession = true config.OTRAutoTearDown = false // List well known Tor hidden services. knownTorDomain := map[string]string{ "jabber.ccc.de": "okj7xc6j2szr2y75.onion", "riseup.net": "4cjw6cwpeaeppfqz.onion", "jabber.calyxinstitute.org": "ijeeynrc6x2uy5ob.onion", "jabber.otr.im": "5rgdtlawqkcplz75.onion", "wtfismyip.com": "ofkztxcohimx34la.onion", } // Autoconfigure well known Tor hidden services. if hiddenService, ok := knownTorDomain[domain]; ok && config.UseTor { const torProxyURL = "socks5://127.0.0.1:9050" info(term, "It appears that you are using a well known server and we will use its Tor hidden service to connect.") config.Server = hiddenService config.Port = 5222 config.Proxies = []string{torProxyURL} term.SetPrompt("> ") return true } var proxyStr string proxyDefaultPrompt := ", enter for none" if config.UseTor { proxyDefaultPrompt = ", which is the default" } term.SetPrompt("Proxy (i.e socks5://127.0.0.1:9050" + proxyDefaultPrompt + "): ") for { if proxyStr, err = term.ReadLine(); err != nil { return false } if len(proxyStr) == 0 { if !config.UseTor { break } else { proxyStr = "socks5://127.0.0.1:9050" } } 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("> ") return true }
func enroll(conf *config.ApplicationConfig, currentConf *config.Account, 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 currentConf.Account, err = term.ReadLine(); err != nil || len(currentConf.Account) == 0 { return false } parts := strings.SplitN(currentConf.Account, "@", 2) if len(parts) != 2 { alert(term, "invalid username (want user@domain): "+currentConf.Account) continue } domain = parts[1] break } term.SetPrompt("Enable debug logging to /tmp/xmpp-client-debug.log? ") if debugLog, err := term.ReadLine(); err != nil || !config.ParseYes(debugLog) { info(term, "Not enabling debug logging...") } else { info(term, "Debug logging enabled...") conf.RawLogFile = "/tmp/xmpp-client-debug.log" } term.SetPrompt("Use Tor?: ") if useTorQuery, err := term.ReadLine(); err != nil || len(useTorQuery) == 0 || !config.ParseYes(useTorQuery) { info(term, "Not using Tor...") currentConf.RequireTor = false } else { info(term, "Using Tor...") currentConf.RequireTor = true } term.SetPrompt("File to import libotr private key from (enter to generate): ") var pkeys []otr3.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 } var priv otr3.DSAPrivateKey if !priv.Import(privKeyBytes) { alert(term, "Failed to parse libotr private key file (the parser is pretty simple I'm afraid)") continue } pkeys = append(pkeys, &priv) break } else { info(term, "Generating private key...") pkeys, err = otr3.GenerateMissingKeys([][]byte{}) if err != nil { alert(term, "Failed to generate private key - this implies something is really bad with your system, so we bail out now") return false } break } } currentConf.PrivateKeys = config.SerializedKeys(pkeys) currentConf.OTRAutoAppendTag = true currentConf.OTRAutoStartSession = true currentConf.OTRAutoTearDown = false // Force Tor for servers with well known Tor hidden services. if _, ok := servers.Get(domain); ok && currentConf.RequireTor { const torProxyURL = "socks5://127.0.0.1:9050" info(term, "It appears that you are using a well known server and we will use its Tor hidden service to connect.") currentConf.Proxies = []string{torProxyURL} term.SetPrompt("> ") return true } var proxyStr string proxyDefaultPrompt := ", enter for none" if currentConf.RequireTor { proxyDefaultPrompt = ", which is the default" } term.SetPrompt("Proxy (i.e socks5://127.0.0.1:9050" + proxyDefaultPrompt + "): ") for { if proxyStr, err = term.ReadLine(); err != nil { return false } if len(proxyStr) == 0 { if !currentConf.RequireTor { break } else { proxyStr = "socks5://127.0.0.1:9050" } } 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 } term.SetPrompt("> ") return true }
// promptForForm runs an XEP-0004 form and collects responses from the user. func promptForForm(term *terminal.Terminal, user, password, title, instructions string, fields []interface{}) error { info(term, "The server has requested the following information. Text that has come from the server will be shown in red.") // formStringForPrinting takes a string form the form and returns an // escaped version with codes to make it show as red. formStringForPrinting := func(s string) string { var line []byte line = append(line, term.Escape.Red...) line = appendTerminalEscaped(line, []byte(s)) line = append(line, term.Escape.Reset...) return string(line) } write := func(s string) { term.Write([]byte(s)) } var tmpDir string showMediaEntries := func(questionNumber int, medias [][]xmpp.Media) { if len(medias) == 0 { return } write("The following media blobs have been provided by the server with this question:\n") for i, media := range medias { for j, rep := range media { if j == 0 { write(fmt.Sprintf(" %d. ", i+1)) } else { write(" ") } write(fmt.Sprintf("Data of type %s", formStringForPrinting(rep.MIMEType))) if len(rep.URI) > 0 { write(fmt.Sprintf(" at %s\n", formStringForPrinting(rep.URI))) continue } var fileExt string switch rep.MIMEType { case "image/png": fileExt = "png" case "image/jpeg": fileExt = "jpeg" } if len(tmpDir) == 0 { var err error if tmpDir, err = ioutil.TempDir("", "xmppclient"); err != nil { write(", but failed to create temporary directory in which to save it: " + err.Error() + "\n") continue } } filename := filepath.Join(tmpDir, fmt.Sprintf("%d-%d-%d", questionNumber, i, j)) if len(fileExt) > 0 { filename = filename + "." + fileExt } out, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { write(", but failed to create file in which to save it: " + err.Error() + "\n") continue } out.Write(rep.Data) out.Close() write(", saved in " + filename + "\n") } } write("\n") } var err error if len(title) > 0 { write(fmt.Sprintf("Title: %s\n", formStringForPrinting(title))) } if len(instructions) > 0 { write(fmt.Sprintf("Instructions: %s\n", formStringForPrinting(instructions))) } questionNumber := 0 for _, field := range fields { questionNumber++ write("\n") switch field := field.(type) { case *xmpp.FixedFormField: write(formStringForPrinting(field.Text)) write("\n") questionNumber-- case *xmpp.BooleanFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) term.SetPrompt("Please enter yes, y, no or n: ") TryAgain: for { answer, err := term.ReadLine() if err != nil { return err } switch answer { case "yes", "y": field.Result = true case "no", "n": field.Result = false default: continue TryAgain } break } case *xmpp.TextFormField: switch field.Label { case "CAPTCHA web page": if strings.HasPrefix(field.Default, "http") { // This is a oddity of jabber.ccc.de and maybe // others. The URL for the capture is provided // as the default answer to a question. Perhaps // that was needed with some clients. However, // we support embedded media and it's confusing // to ask the question, so we just print the // URL. write(fmt.Sprintf("CAPTCHA web page (only if not provided below): %s\n", formStringForPrinting(field.Default))) questionNumber-- continue } case "User": field.Result = user questionNumber-- continue case "Password": field.Result = password questionNumber-- continue } write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) if len(field.Default) > 0 { write(fmt.Sprintf("Please enter response or leave blank for the default, which is '%s'\n", formStringForPrinting(field.Default))) } else { write("Please enter response") } term.SetPrompt("> ") if field.Private { field.Result, err = term.ReadPassword("> ") } else { field.Result, err = term.ReadLine() } if err != nil { return err } if len(field.Result) == 0 { field.Result = field.Default } case *xmpp.MultiTextFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) write("Please enter one or more responses, terminated by an empty line\n") term.SetPrompt("> ") for { line, err := term.ReadLine() if err != nil { return err } if len(line) == 0 { break } field.Results = append(field.Results, line) } case *xmpp.SelectionFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) for i, opt := range field.Values { write(fmt.Sprintf(" %d. %s\n\n", i+1, formStringForPrinting(opt))) } term.SetPrompt("Please enter the number of your selection: ") TryAgain2: for { answer, err := term.ReadLine() if err != nil { return err } answerNum, err := strconv.Atoi(answer) answerNum-- if err != nil || answerNum < 0 || answerNum >= len(field.Values) { write("Cannot parse that reply. Try again.") continue TryAgain2 } field.Result = answerNum break } case *xmpp.MultiSelectionFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) for i, opt := range field.Values { write(fmt.Sprintf(" %d. %s\n\n", i+1, formStringForPrinting(opt))) } term.SetPrompt("Please enter the numbers of zero or more of the above, separated by spaces: ") TryAgain3: for { answer, err := term.ReadLine() if err != nil { return err } var candidateResults []int answers := strings.Fields(answer) for _, answerStr := range answers { answerNum, err := strconv.Atoi(answerStr) answerNum-- if err != nil || answerNum < 0 || answerNum >= len(field.Values) { write("Cannot parse that reply. Please try again.") continue TryAgain3 } for _, other := range candidateResults { if answerNum == other { write("Cannot have duplicates. Please try again.") continue TryAgain3 } } candidateResults = append(candidateResults, answerNum) } field.Results = candidateResults break } } } if len(tmpDir) > 0 { os.RemoveAll(tmpDir) } return nil }
func readLine(shell *terminal.Terminal, t *testing.T) { if _, err := shell.ReadLine(); err != nil && err != io.EOF { t.Errorf("unable to read line: %v", err) } }
func main() { var ( filename string site string username string password string line string buf []byte db *keyrack.Database oldState *terminal.State term *terminal.Terminal termio TermIO group *keyrack.Group //trail []*keyrack.Group groupView GroupView matched bool quit bool err error ) if len(os.Args) != 2 { fmt.Printf("Syntax: %s <filename>\n", os.Args[0]) os.Exit(1) } filename = os.Args[1] // setup terminal oldState, err = terminal.MakeRaw(0) if err != nil { panic(err) } defer terminal.Restore(0, oldState) termio.Input = os.Stdin termio.Output = os.Stdout term = terminal.NewTerminal(termio, "> ") if _, err = os.Stat(filename); os.IsNotExist(err) { db, err = keyrack.NewDatabase() } else { password, err = term.ReadPassword("Password: "******"" } group = db.Top() for err == nil && !quit { groupView.Group = group buf, err = groupView.Render() if err != nil { break } _, err = term.Write(buf) if err != nil { break } line, err = term.ReadLine() if err != nil { break } switch line { case "q": password, err = term.ReadPassword("Password: "******"" quit = true case "ng", "new group": term.SetPrompt("Group name: ") line, err = term.ReadLine() if err != nil { break } if line == "" { term.Write([]byte("Group creation cancelled.\n")) } else { err = group.AddGroup(line) if err != nil { if err == keyrack.ErrGroupExists { term.Write([]byte("Group already exists.\n")) } else { break } } } term.SetPrompt("> ") case "nl", "new login": term.SetPrompt("Site name: ") site, err = term.ReadLine() if err != nil { break } if site == "" { term.Write([]byte("Login creation cancelled.\n")) } else { term.SetPrompt("Username: "******"" { term.Write([]byte("Login creation cancelled.\n")) } else { password, err = term.ReadPassword("Password: "******"" { term.Write([]byte("Login creation cancelled.\n")) } else { err = group.AddLogin(site, username, password) password = "" if err != nil { if err == keyrack.ErrLoginExists { term.Write([]byte("Login already exists.\n")) } else { break } } } } } term.SetPrompt("> ") default: matched, err = regexp.MatchString("^G(\\d+)$", line) if err != nil { break } if matched { term.Write([]byte("Group!\n")) continue } matched, err = regexp.MatchString("^L(\\d+)$", line) if err != nil { break } if matched { term.Write([]byte("Group!\n")) } } } if err != nil { fmt.Println(err) } if quit { fmt.Println("Quitting...") } }