Skip to content

Commit

Permalink
Implement host provider register and ability to disable providers
Browse files Browse the repository at this point in the history
  • Loading branch information
deanishe committed May 27, 2016
1 parent ba80db0 commit 65ae494
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 106 deletions.
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ Open SSH connections from [Alfred 3][alfredapp] (only) with autosuggestions base
Features
--------

- Auto-suggest hostnames from `/etc/hosts` and `~/.ssh/known_hosts`.
- Remembers usernames, so you don't have to type them in every time. (You can also remove connections from your history.)
- Auto-suggest hostnames from `/etc/hosts` and `~/.ssh/known_hosts` (sources can be individually disabled).
- Remembers usernames, so you don't have to type them in every time. (You can also remove connections from your history or disable it entirely.)
- Alternate actions:
- Open SFTP connection instead of SSH.
- Ping host.

This started as a straight port of [@isometry's][isometry] Python [SSH workflow][ssh-breathe] to Go as a testbed for the language and a Go workflow library. It has since been ported to Alfred-3 only, and gained some additional features.
This started as a straight port of [@isometry's][isometry] Python [SSH workflow][ssh-breathe] to Go as a testbed for the language and a Go workflow library. It has since been ported to Alfred 3 only, and gained some additional features.


Installation
Expand All @@ -38,6 +38,20 @@ Keyword is `ssh`:
- `⇧+↩` — Ping host.
- `⌥+↩` — Forget connection (if it's from history).

### Configuration

There are several options available in the workflow's configuration sheet. Notably, you can turn off individual autosuggestion sources.

| Variable | Description |
|:----------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `DISABLE_ETC_HOSTS` | Set to `1` to turn off suggestions from `/etc/hosts`. |
| `DISABLE_KNOWN_HOSTS` | Set to `1` to turn off suggestions from `~/.ssh/known_hosts`. |
| `DISABLE_HISTORY` | Set to `1` to disable the History (reading and writing). |
| `EXTERNAL_TRIGGER` | Set to `1` to re-open Alfred via an External Trigger instead of a direct AppleScript call. The External Trigger is safer, but leaves Alfred in a weird mode. |




**Please note**: The workflow simply generates an `ssh://` (or `sftp://`) URL and asks Alfred to open it. Similarly, the ping function uses Alfred 3's Terminal Command feature. If it's not opening in the right app, it's not the workflow's fault.


Expand All @@ -53,6 +67,9 @@ The icon is based on [Octicons][octicons] by [Github][gh], released under the [S
Changelog
---------

- v0.4.0 — 2016-05-27
- Add ability to turn sources of suggestions off #1

- v0.3.0 — 2016-05-26
- Alternate action: Open SFTP connection
- Alternate action: Ping host
Expand Down
Binary file removed Secure-SHell-0.3.0.alfredworkflow
Binary file not shown.
Binary file added Secure-SHell-0.4.0.alfredworkflow
Binary file not shown.
157 changes: 95 additions & 62 deletions alfred-ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,72 @@ import (
)

var (
// minFuzzyScore is the default cut-off for search results
minFuzzyScore = 30.0
usage = `alfssh [options] [<query>]
Display a list of know SSH hosts in Alfred 3. If <query>
is specified, the hostnames will be filtered against it.
Usage:
alfssh search [-d] [<query>]
alfssh remember <url>
alfssh print (datadir|cachedir|distname|logfile)
alfssh --help|--version
Options:
-h, --help Show this message and exit.
--version Show version information and exit.
-d, --demo Use fake test data instead of real data from the computer.
Useful for testing, otherwise pointless. Demo mode can also
turned on by setting the environment variable DEMO_MODE=1
`
// knownHostsPath string
knownHostsPath = os.ExpandEnv("$HOME/.ssh/known_hosts")
etcHostsPath = "/etc/hosts"
// wf *workflow.Workflow
// Providers contains all registered providers of Hosts
Providers map[string]Provider
// disabled contains the names of disabled providers
disabled map[string]bool
)

// func init() {
// wf = workflow.NewWorkflow(nil)
// }
func init() {
Providers = make(map[string]Provider, 2)
disabled = map[string]bool{}
Register(&providerWrapper{name: "/etc/hosts", fn: readEtcHosts})
Register(&providerWrapper{name: "known_hosts", fn: readKnownHosts})
}

// --------------------------------------------------------------------
// Data models
// --------------------------------------------------------------------

// Provider is a provider of Hosts.
type Provider interface {
Name() string
Hosts() []*Host
}

// Disable disables a Provider.
func Disable(name string) {
if p := Providers[name]; p != nil {
disabled[name] = true
} else {
log.Printf("Unknown provider: %s", name)
}
}

// Disabled returns true if a Provider is disabled.
func Disabled(name string) bool {
return disabled[name]
}

// Register registers a Provider.
func Register(p Provider) {
Providers[p.Name()] = p
}

type providerWrapper struct {
name string
fn func() []*Host
hosts []*Host
called bool
}

// Name implements Provider.
func (pw *providerWrapper) Name() string {
return pw.name
}

// Hosts implements Provider.
func (pw *providerWrapper) Hosts() []*Host {
if pw.called {
return pw.hosts
}
pw.hosts = pw.fn()
pw.called = true
return pw.hosts
}

// Host is computer that may be connected to.
type Host struct {
Hostname string `json:"host"`
Expand Down Expand Up @@ -160,7 +192,7 @@ func (h *History) Add(URL string) error {
log.Printf("Not adding connection without username to history: %v", URL)
return nil
}
host.Source = "history"
host.Source = h.Name()
h.hosts = append(h.hosts, host)

log.Printf("Adding %s to history ...", host.Name())
Expand All @@ -186,6 +218,11 @@ func (h *History) Hosts() []*Host {
return h.hosts
}

// Name implements Provider.
func (h *History) Name() string {
return "history"
}

// Load loads the history from disk.
func (h *History) Load() error {
if _, err := os.Stat(h.Path); err != nil {
Expand All @@ -210,7 +247,7 @@ func (h *History) Load() error {
h.hosts = append(h.hosts, host)
}
}
log.Printf("%d host(s) in history", len(h.hosts))
// log.Printf("%d host(s) in history", len(h.hosts))
return nil
}

Expand All @@ -235,6 +272,17 @@ func (h *History) Save() error {
return nil
}

// RegisterHistory is a convenience method to create and register a History.
func RegisterHistory(path string) (*History, error) {
h := NewHistory(path)
if err := h.Load(); err != nil {
return nil, err
}
Register(h)

return h, nil
}

// --------------------------------------------------------------------
// Load data
// --------------------------------------------------------------------
Expand Down Expand Up @@ -318,7 +366,7 @@ func readKnownHosts() []*Host {
}
}

log.Printf("%d host(s) in ~/.ssh/known_hosts", len(hosts))
// log.Printf("%d host(s) in ~/.ssh/known_hosts", len(hosts))
return hosts
}

Expand Down Expand Up @@ -366,53 +414,38 @@ func readEtcHosts() []*Host {
}
}

log.Printf("%d host(s) in /etc/hosts", len(hosts))
// log.Printf("%d host(s) in /etc/hosts", len(hosts))
return hosts
}

// Hosts loads hosts from all sources. Duplicates are removed.
// Hosts loads Hosts from active providers. Duplicates are removed.
func Hosts() []*Host {
hosts := []*Host{}

seen := make(map[string]bool, 10)

// ~/.ssh/known_hosts
for _, h := range readKnownHosts() {

url := h.URL()

if _, dupe := seen[url]; !dupe {
hosts = append(hosts, h)
seen[url] = true
for n, p := range Providers {
if Disabled(n) {
log.Printf("Ignoring disabled provider '%s'", n)
continue
}
}

// /etc/hosts
for _, h := range readEtcHosts() {
i := 0
j := 0
for _, h := range p.Hosts() {

url := h.URL()
u := h.URL()

if _, dupe := seen[u]; !dupe {
hosts = append(hosts, h)
seen[u] = true
i++
} else {
j++
}

if _, dupe := seen[url]; !dupe {
hosts = append(hosts, h)
seen[url] = true
}
log.Printf("Loaded %d host(s) from '%s', ignored %d dupe(s)", i, n, j)
}

// // History
// h := NewHistory(filepath.Join(wf.DataDir(), "history.json"))
// if err := h.Load(); err != nil {
// log.Printf("Error loading history: %v", err)
// } else {
// for _, h := range h.hosts {
//
// url := h.URL()
//
// if _, dupe := seen[url]; !dupe {
// hosts = append(hosts, h)
// seen[url] = true
// }
// }
// }
// sort.Sort(hosts)
return hosts
}
31 changes: 23 additions & 8 deletions cmd/alfssh/alfssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func runOptions() *options {

// Parse options --------------------------------------------------
vstr := fmt.Sprintf("%s/%v (awgo/%v)", workflow.Name(),
workflow.Version(), workflow.LibVersion)
workflow.Version(), workflow.AwgoVersion)

args, err := docopt.Parse(usage, nil, true, vstr, false)
if err != nil {
Expand Down Expand Up @@ -144,7 +144,7 @@ func run() {

var hosts Hosts
var host *assh.Host
var h *assh.History
// var h *assh.History
var historyPath string

o := runOptions()
Expand Down Expand Up @@ -183,6 +183,11 @@ func run() {

} else if o.url != "" { // Remember or forget URL

if os.Getenv("DISABLE_HISTORY") == "1" {
log.Println("History disabled. Ignoring.")
return
}

h := assh.NewHistory(historyPath)
if err := h.Load(); err != nil {
log.Printf("Error loading history : %v", err)
Expand Down Expand Up @@ -221,11 +226,21 @@ func run() {
// Load hosts -----------------------------------------------------

// History
h = assh.NewHistory(historyPath)
if err := h.Load(); err != nil {
_, err := assh.RegisterHistory(historyPath)
if err != nil {
log.Printf("Error loading history : %v", err)
}
hosts = h.Hosts()

// Disable sources user doesn't want
if os.Getenv("DISABLE_HISTORY") == "1" {
assh.Disable("history")
}
if os.Getenv("DISABLE_ETC_HOSTS") == "1" {
assh.Disable("/etc/hosts")
}
if os.Getenv("DISABLE_KNOWN_HOSTS") == "1" {
assh.Disable("known_hosts")
}

// Main dataset
if o.useTestData {
Expand Down Expand Up @@ -310,12 +325,12 @@ func run() {
// Modifiers -----------------------------------------------------

// Open SFTP connection instead
m, _ := it.NewModifier("cmd")
m := it.NewModifier("cmd")
m.SetArg(host.SFTP())
m.SetSubtitle(fmt.Sprintf("Open as SFTP connection (%s)", host.SFTP()))

// Delete connection from history
m, _ = it.NewModifier("alt")
m = it.NewModifier("alt")
if host.Source == "history" {
m.SetSubtitle("Delete connection from history")
m.SetValid(true)
Expand All @@ -326,7 +341,7 @@ func run() {
}

// Ping host
m, _ = it.NewModifier("shift")
m = it.NewModifier("shift")
m.SetSubtitle(fmt.Sprintf("Ping %s", host.Hostname))
m.SetArg(host.Hostname)

Expand Down
Loading

0 comments on commit 65ae494

Please sign in to comment.