9 |
10 | ## Install
11 |
12 | You can use a pre-built binary from releases or build your own from source.
13 |
14 | ## Configuration
15 | You can set these as environment variables prefixed with `FINGERTIP_` or store it in the app config directory as `fingertip.env`
16 |
17 | ```
18 | # letsdane proxy address
19 | PROXY_ADDRESS=127.0.0.1:9590
20 | # hnsd root server address
21 | ROOT_ADDRESS=127.0.0.1:9591
22 | # hnsd recursive resolver address
23 | RECURSIVE_ADDRESS=127.0.0.1:9592
24 | # Connect your own Ethereum full node/or blockchain provider such as Infura
25 | #ETHEREUM_ENDPOINT=/home/user/.ethereum/geth.ipc or
26 | #ETHEREUM_ENDPOINT=https://mainnet.infura.io/v3/YOUR-PROJECT-ID
27 | ```
28 |
29 | ## Build from source
30 |
31 | Go 1.16+ is required.
32 |
33 | ```
34 | $ git clone https://github.com/buffrr/fingertip
35 | ```
36 |
37 | ### MacOS
38 |
39 | ```
40 | $ brew install dylibbundler git automake autoconf libtool unbound
41 | $ git clone https://github.com/imperviousinc/fingertip
42 | $ cd fingertip && ./builds/macos/build.sh
43 | ```
44 |
45 | For development, you can run fingertip from the following path:
46 | ```
47 | $ ./builds/macos/Fingertip.app/Contents/MacOS/fingertip
48 | ```
49 |
50 | Configure your IDE to output to this directory or continue to use `build.sh` when making changes (it will only build hnsd once).
51 |
52 | ### Windows
53 |
54 | Follow [hnsd](https://github.com/handshake-org/hnsd) build instructions for windows. Copy hnsd.exe binary and its dependencies (libcrypto, libssl and libunbound dlls) into the `fingertip/builds/windows` directory.
55 | You no longer need to use MSYS shell.
56 |
57 | ```
58 | $ choco install mingw
59 | $ go build -trimpath -o ./builds/windows/ -ldflags "-H windowsgui"
60 | ```
61 |
62 | ### Linux
63 |
64 | Follow [hnsd](https://github.com/handshake-org/hnsd) build instructions for Linux. Copy hnsd binary into the `fingertip/builds/linux/appdir/usr/bin` directory.
65 |
66 | ```
67 | $ go build -trimpath -o ./builds/linux/appdir/usr/bin/
68 | ```
69 |
70 |
71 | ## Credits
72 | Fingertip uses [hnsd](https://github.com/handshake-org/hnsd) a lightweight Handshake resolver, [letsdane](https://github.com/buffrr/letsdane) for TLS support and [go-ethereum](https://github.com/ethereum/go-ethereum) for .eth and Ethereum [HIP-5](https://github.com/handshake-org/HIPs/blob/master/HIP-0005.md) lookups.
73 |
74 | The name "fingertip" was stolen from [@pinheadmz](https://github.com/pinheadmz)
75 |
--------------------------------------------------------------------------------
/internal/config/user.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "github.com/spf13/viper"
8 | "io/ioutil"
9 | "os"
10 | )
11 |
12 | const (
13 | DefaultProxyAddr = "127.0.0.1:9590"
14 | DefaultRootAddr = "127.0.0.1:9591"
15 | DefaultRecursiveAddr = "127.0.0.1:9592"
16 | DefaultEthereumEndpoint = "https://mainnet.infura.io/v3/b0933ce6026a4e1e80e89e96a5d095bc"
17 | )
18 |
19 | // User Represents user facing configuration
20 | type User struct {
21 | ProxyAddr string `mapstructure:"PROXY_ADDRESS"`
22 | RootAddr string `mapstructure:"ROOT_ADDRESS"`
23 | RecursiveAddr string `mapstructure:"RECURSIVE_ADDRESS"`
24 | EthereumEndpoint string `mapstructure:"ETHEREUM_ENDPOINT"`
25 | }
26 |
27 | // Stored config
28 | type Store struct {
29 | Version string `json:"version"`
30 | AutoConfig bool `json:"auto_config"`
31 |
32 | path string
33 | }
34 |
35 | func readStore(path, version string, old *Store) (*Store, error) {
36 | var zero *Store
37 | if old != nil {
38 | zero = old
39 | } else {
40 | zero = &Store{
41 | AutoConfig: false,
42 | Version: version,
43 | path: path,
44 | }
45 | }
46 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
47 | return zero, nil
48 | }
49 |
50 | b, err := ioutil.ReadFile(path)
51 | if err != nil {
52 | return nil, fmt.Errorf("failed reading app config: %v", err)
53 | }
54 | if len(b) == 0 {
55 | return zero, nil
56 | }
57 | if err := json.Unmarshal(b, zero); err != nil {
58 | return nil, fmt.Errorf("failed parsing app config: %v", err)
59 | }
60 | return zero, nil
61 | }
62 |
63 | func (i *Store) Reload() error {
64 | _, err := readStore(i.path, i.Version, i)
65 | return err
66 | }
67 |
68 | func (i *Store) Save() error {
69 | b, err := json.Marshal(i)
70 | if err != nil {
71 | return fmt.Errorf("failed encoding app config: %v", err)
72 | }
73 |
74 | if err := ioutil.WriteFile(i.path, b, 0664); err != nil {
75 | return fmt.Errorf("faild writing app config: %v", err)
76 | }
77 | return err
78 | }
79 |
80 | var ErrUserConfigNotFound = errors.New("user config not found")
81 |
82 | // ReadUserConfig reads user facing configuration
83 | func ReadUserConfig(path string) (config User, err error) {
84 | // TODO: Viper is likely overkill write a custom loader
85 | viper.AddConfigPath(path)
86 | viper.SetConfigName("fingertip.env")
87 | viper.SetConfigType("env")
88 | viper.SetEnvPrefix("FINGERTIP")
89 | viper.AutomaticEnv()
90 |
91 | viper.SetDefault("PROXY_ADDRESS", DefaultProxyAddr)
92 | viper.SetDefault("ROOT_ADDRESS", DefaultRootAddr)
93 | viper.SetDefault("RECURSIVE_ADDRESS", DefaultRecursiveAddr)
94 | viper.SetDefault("ETHEREUM_ENDPOINT", DefaultEthereumEndpoint)
95 |
96 | err = viper.ReadInConfig()
97 | if err != nil {
98 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
99 | err = fmt.Errorf("error reading user config: %v", err)
100 | return
101 | }
102 | }
103 |
104 | err = viper.Unmarshal(&config)
105 | if err != nil {
106 | err = fmt.Errorf("error reading user config: %v", err)
107 | }
108 | return
109 | }
110 |
--------------------------------------------------------------------------------
/internal/ui/tray.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "fingertip/internal/config"
5 | "fingertip/internal/ui/icon"
6 | "fmt"
7 | "github.com/getlantern/systray"
8 | "sync"
9 | )
10 |
11 | var (
12 | OnExit func()
13 | OnStart func()
14 | OnAutostart func(checked bool) bool
15 | OnConfigureOS func(checked bool) bool
16 | OnOpenHelp func()
17 | OnStop func()
18 | OnReady func()
19 | Data State
20 | )
21 |
22 | func Loop() {
23 | systray.Run(Data.initMenu, OnExit)
24 | }
25 |
26 | type State struct {
27 | started bool
28 | runToggle *systray.MenuItem
29 | openAtLogin *systray.MenuItem
30 | blockHeight *systray.MenuItem
31 | options *systray.MenuItem
32 | quit *systray.MenuItem
33 |
34 | autoConfig *systray.MenuItem
35 | openSetup *systray.MenuItem
36 |
37 | sync.RWMutex
38 | }
39 |
40 | // space padding for width
41 | var startTitle = fmt.Sprintf("%-35s", "Start")
42 | var stopTitle = fmt.Sprintf("%-35s", "Stop")
43 |
44 | func (s *State) SetBlockHeight(h string) {
45 | s.Lock()
46 | defer s.Unlock()
47 | if s.blockHeight == nil {
48 | return
49 | }
50 | s.blockHeight.SetTitle("Block height " + h)
51 | }
52 |
53 | func (s *State) Started() bool {
54 | s.RLock()
55 | defer s.RUnlock()
56 |
57 | return s.started
58 | }
59 |
60 | func (s *State) toggleStarted() bool {
61 | if s.Started() {
62 | s.SetStarted(false)
63 | return false
64 | }
65 |
66 | return true
67 | }
68 |
69 | func (s *State) SetStarted(started bool) {
70 | s.Lock()
71 | defer s.Unlock()
72 |
73 | s.started = started
74 |
75 | if s.started {
76 | s.runToggle.SetTitle(stopTitle)
77 | } else {
78 | s.runToggle.SetTitle(startTitle)
79 | s.blockHeight.SetTitle("Block height --")
80 | }
81 | }
82 |
83 | func (s *State) SetOpenAtLogin(checked bool) {
84 | if checked {
85 | s.openAtLogin.Check()
86 | return
87 | }
88 | s.openAtLogin.Uncheck()
89 | }
90 |
91 | func (s *State) OpenAtLogin() bool {
92 | return s.openAtLogin.Checked()
93 | }
94 |
95 | func (s *State) SetAutoConfig(checked bool) {
96 | if checked {
97 | s.autoConfig.Check()
98 | return
99 | }
100 |
101 | s.autoConfig.Uncheck()
102 | }
103 |
104 | func (s *State) SetAutoConfigEnabled(enabled bool) {
105 | if enabled {
106 | s.autoConfig.Enable()
107 | return
108 | }
109 |
110 | s.autoConfig.Disable()
111 | }
112 |
113 | func (s *State) SetOptionsEnabled(enabled bool) {
114 | if enabled {
115 | s.options.Enable()
116 | return
117 | }
118 | s.options.Disable()
119 | }
120 |
121 | func (s *State) initMenu() {
122 | systray.SetTemplateIcon(icon.Toolbar, icon.Toolbar)
123 | systray.SetTooltip(config.AppName)
124 |
125 | s.runToggle = systray.AddMenuItem(startTitle, "")
126 | s.openAtLogin = systray.AddMenuItemCheckbox("Open at login", "Open at login", false)
127 |
128 | systray.AddSeparator()
129 | s.blockHeight = systray.AddMenuItem("Block height --", "Block height")
130 | s.blockHeight.Disable()
131 |
132 | systray.AddSeparator()
133 | s.options = systray.AddMenuItem("Options", "")
134 |
135 | s.autoConfig = s.options.AddSubMenuItemCheckbox("Auto configure", "", false)
136 | s.openSetup = s.options.AddSubMenuItem("Help", "")
137 |
138 | s.quit = systray.AddMenuItem("Quit", "")
139 |
140 | OnReady()
141 |
142 | go func() {
143 | for {
144 | select {
145 | case <-s.runToggle.ClickedCh:
146 | if s.toggleStarted() {
147 | OnStart()
148 | continue
149 | }
150 |
151 | OnStop()
152 | case <-s.openAtLogin.ClickedCh:
153 | s.SetOpenAtLogin(OnAutostart(s.openAtLogin.Checked()))
154 | case <-s.autoConfig.ClickedCh:
155 | s.SetAutoConfig(OnConfigureOS(s.autoConfig.Checked()))
156 | case <-s.openSetup.ClickedCh:
157 | OnOpenHelp()
158 | continue
159 | case <-s.quit.ClickedCh:
160 | systray.Quit()
161 | return
162 | }
163 | }
164 | }()
165 | }
166 |
--------------------------------------------------------------------------------
/internal/config/auto/install.go:
--------------------------------------------------------------------------------
1 | package auto
2 |
3 | import (
4 | "bufio"
5 | "crypto/x509"
6 | "encoding/pem"
7 | "errors"
8 | "fmt"
9 | "io/fs"
10 | "io/ioutil"
11 | "os"
12 | "path"
13 | "strings"
14 | )
15 |
16 | type ProxyStatus int
17 |
18 | const (
19 | ProxyStatusNone ProxyStatus = 0
20 | ProxyStatusInstalled = 1
21 | ProxyStatusConflict = 2
22 | )
23 |
24 | // firefox
25 | const (
26 | userProfileHeader = "// AUTOCONFIG:FINGERTIP"
27 | userPref = userProfileHeader +
28 | ` (autogenerated - remove this line to disable auto config for this profile and edit the file)
29 | user_pref("security.enterprise_roots.enabled", true);
30 | user_pref("network.proxy.type", 5);
31 | `
32 | )
33 |
34 | func equalURL(a, b string) bool {
35 | a = strings.TrimSuffix(strings.TrimSpace(a), "/")
36 | b = strings.TrimSuffix(strings.TrimSpace(b), "/")
37 | return strings.EqualFold(a, b)
38 | }
39 |
40 | // ConfigureFirefox instructs firefox to read system certs and proxy
41 | // settings
42 | func ConfigureFirefox() error {
43 | profiles, err := getProfilePaths()
44 | if err != nil {
45 | return err
46 | }
47 |
48 | var lastErr error
49 | for _, profilePath := range profiles {
50 | if err := writeUserConfig(profilePath); err != nil {
51 | lastErr = err
52 | }
53 | }
54 | return lastErr
55 | }
56 |
57 | // UndoFirefoxConfiguration undoes all actions made by ConfigureFirefox
58 | func UndoFirefoxConfiguration() {
59 | profiles, err := getProfilePaths()
60 | if err != nil {
61 | return
62 | }
63 |
64 | for _, profilePath := range profiles {
65 | userjs := path.Join(profilePath, "user.js")
66 | if ok, _, _ := fileLineContains(userjs, userProfileHeader); ok {
67 | _ = os.Remove(userjs)
68 | }
69 | }
70 | }
71 |
72 | func writeUserConfig(profilePath string) (err error) {
73 | prefs := path.Join(profilePath, "prefs.js")
74 | // if user has existing proxy configuration
75 | // ignore this profile
76 | if ok, line, _ := fileLineContains(prefs, `"network.proxy.type"`); ok {
77 | // loosely check if its type 0 (no proxy) or 5 (system proxy)
78 | if !strings.ContainsRune(line, '0') && !strings.ContainsRune(line, '5') {
79 | return errors.New("profile with existing proxy configuration")
80 | }
81 | }
82 |
83 | userjs := path.Join(profilePath, "user.js")
84 | if _, err = os.Stat(userjs); err == nil {
85 | if ok, _, _ := fileLineContains(userjs, userProfileHeader); !ok {
86 | return nil
87 | }
88 | }
89 |
90 | if err != nil && !errors.Is(err, fs.ErrNotExist) {
91 | return fmt.Errorf("failed checking whether file exists: %v", err)
92 | }
93 |
94 | return os.WriteFile(userjs, []byte(userPref), 0644)
95 | }
96 |
97 | func fileLineContains(file, substr string) (bool, string, error) {
98 | f, err := os.Open(file)
99 | if err != nil {
100 | return false, "", err
101 | }
102 | defer f.Close()
103 |
104 | sc := bufio.NewScanner(f)
105 | for sc.Scan() {
106 | line := sc.Text()
107 | if strings.Contains(line, substr) {
108 | return true, line, nil
109 | }
110 | }
111 | return false, "", nil
112 | }
113 |
114 | func getProfilePaths() ([]string, error) {
115 | c, err := os.UserConfigDir()
116 | if err != nil {
117 | return nil, fmt.Errorf("failed reading user config dir: %v", err)
118 | }
119 |
120 | pp := path.Join(c, relativeProfilesPath)
121 | dirs, err := os.ReadDir(pp)
122 | if err != nil {
123 | return nil, fmt.Errorf("failed listing profiles: %v", err)
124 | }
125 |
126 | var paths []string
127 | for _, dir := range dirs {
128 | if strings.Contains(dir.Name(), "default") {
129 | paths = append(paths, path.Join(pp, dir.Name()))
130 | }
131 | }
132 |
133 | return paths, nil
134 | }
135 |
136 | func readPEM(certPath string) ([]byte, error) {
137 | cert, err := ioutil.ReadFile(certPath)
138 | if err != nil {
139 | return nil, fmt.Errorf("failed reading root certificate: %v", err)
140 | }
141 |
142 | // Decode PEM
143 | certBlock, _ := pem.Decode(cert)
144 | if certBlock == nil || certBlock.Type != "CERTIFICATE" {
145 | return nil, fmt.Errorf("failed decoding cert invalid PEM data")
146 | }
147 |
148 | return certBlock.Bytes, nil
149 | }
150 |
151 | func readX509Cert(certPath string) (*x509.Certificate, error) {
152 | cert, err := readPEM(certPath)
153 | if err != nil {
154 | return nil, err
155 | }
156 | c, err := x509.ParseCertificate(cert)
157 | if err != nil {
158 | return nil, err
159 | }
160 |
161 | return c, nil
162 | }
163 |
--------------------------------------------------------------------------------
/internal/config/auto/install_darwin.go:
--------------------------------------------------------------------------------
1 | //+build darwin
2 |
3 | package auto
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "path"
10 | "regexp"
11 | "strings"
12 | )
13 |
14 | var re = regexp.MustCompile(`(?m)^\([0-9]+\)(.+)`)
15 | var relativeProfilesPath = "Firefox/Profiles"
16 |
17 | func VerifyCert(certPath string) (err error) {
18 | // TODO: use Go API once supported
19 | // current api doesn't support reloading
20 | // https://github.com/golang/go/issues/46287
21 | _, err = runCommand("security", "verify-cert", "-c", certPath)
22 | return err
23 | }
24 |
25 | // Supported whether auto configuration
26 | // is supported for this build
27 | func Supported() bool {
28 | return true
29 | }
30 |
31 | func getProxyStatusForService(service, autoURL string) (ProxyStatus, error) {
32 | out, err := runCommand("networksetup", "-getautoproxyurl", service)
33 | if err != nil {
34 | return ProxyStatusConflict, err
35 | }
36 |
37 | url, enabled := parseGetAutoURL(string(out))
38 | if !enabled {
39 | return ProxyStatusNone, nil
40 | }
41 |
42 | if url == "" {
43 | return ProxyStatusNone, nil
44 | }
45 |
46 | if !equalURL(url, autoURL) {
47 | return ProxyStatusConflict, nil
48 | }
49 |
50 | return ProxyStatusInstalled, nil
51 | }
52 |
53 | func UninstallAutoProxy(autoURL string) {
54 | services, err := getNetworkServices()
55 | if err != nil {
56 | return
57 | }
58 |
59 | for _, service := range services {
60 | status, err := getProxyStatusForService(service, autoURL)
61 | if err != nil {
62 | continue
63 | }
64 |
65 | if status == ProxyStatusInstalled {
66 | _, _ = runCommand("networksetup", "-setautoproxystate", service, "off")
67 | }
68 | }
69 | }
70 |
71 | func InstallAutoProxy(autoURL string) error {
72 | services, err := getNetworkServices()
73 | if err != nil {
74 | return fmt.Errorf("failed reading network services")
75 | }
76 |
77 | var lastErr error
78 | installed := 0
79 |
80 | for _, service := range services {
81 | status, err := getProxyStatusForService(service, autoURL)
82 | if err != nil {
83 | lastErr = fmt.Errorf("failed checking proxy status")
84 | continue
85 | }
86 | if status == ProxyStatusConflict {
87 | lastErr = fmt.Errorf("auto configuration failed your OS has existing proxy settings")
88 | }
89 | if status != ProxyStatusNone {
90 | continue
91 | }
92 | if _, err = runCommand("networksetup", "-setautoproxyurl", service, autoURL); err != nil {
93 | lastErr = fmt.Errorf("failed configuring proxy make sure your user account has permissions to change proxy settings")
94 | continue
95 | }
96 | if _, err = runCommand("networksetup", "-setautoproxystate", service, "on"); err != nil {
97 | lastErr = fmt.Errorf("failed changing proxy state for service %s", service)
98 | continue
99 | }
100 |
101 | installed++
102 | }
103 |
104 | if installed > 0 {
105 | return nil
106 | }
107 |
108 | return lastErr
109 | }
110 |
111 | func runCommand(name string, args ...string) ([]byte, error) {
112 | cmd := exec.Command(name, args...)
113 | return cmd.CombinedOutput()
114 | }
115 |
116 | func getNetworkServices() ([]string, error) {
117 | cmd := exec.Command("networksetup", "-listnetworkserviceorder")
118 | out, err := cmd.CombinedOutput()
119 | if err != nil {
120 | return nil, fmt.Errorf("failed reading network services: %v", err)
121 | }
122 |
123 | var services []string
124 | for _, match := range re.FindAllStringSubmatch(string(out), -1) {
125 | if len(match) != 2 {
126 | continue
127 | }
128 | services = append(services, strings.TrimSpace(match[1]))
129 | }
130 |
131 | return services, nil
132 | }
133 |
134 | func InstallCert(certPath string) error {
135 | dir, err := os.UserHomeDir()
136 | if err != nil {
137 | return err
138 | }
139 |
140 | kp := path.Join(dir, "Library/Keychains/login.keychain")
141 | out, err := runCommand("security", "add-trusted-cert",
142 | "-p", "basic", "-p", "ssl", "-k", kp, certPath)
143 |
144 | if err == nil {
145 | return nil
146 | }
147 |
148 | if _, ok := err.(*exec.ExitError); ok {
149 | return fmt.Errorf("failed installing cert: %s", string(out))
150 | }
151 |
152 | return fmt.Errorf("failed installing cert: %v", err)
153 | }
154 |
155 | func UninstallCert(certPath string) error {
156 | _, err := runCommand("security", "remove-trusted-cert", certPath)
157 | return err
158 | }
159 |
160 | // parses networksetup -getautoproxyurl Your private CA is stored at {{.CertPath}}.
140 | It cannot be used to issue certificates for legacy domains (ending with .com, .net ... etc) since it uses the name constraints extension. Add this CA to your browser/TLS client trust store to 141 | allow Fingertip to issue certificates for decentralized names. 142 |
143 | 144 |Choose Automatic Proxy configuration in your browser/TLS client proxy settings and add this url:
150 || Handshake Resolver Status | 143 |Syncing ... | 144 |
| Block height | 147 |-- | 148 |
| Certificate installed | 151 |Checking ... | 152 |
| Browser using Fingertip | 155 |Checking ... | 156 |
| DNS Interference Test | 160 |Checking ... | 161 |