Mercurial > projects > sievemgr
comparison cmd/sievemgr/common.go @ 5:4dff4c3f0fbb
Introduce configuration file where account information is specified
Introduce a configuration file where account information must be specified
instead of passing it with each invocation on the command line. Each account
has a name by which it can be selected and one may be specified as the default
account. This is intended to improve usability for productive usage.
Enforce strict permissions since passwords may be specified for non-interactive
usage.
Remove command-line flags for passing account information.
author | Guido Berhoerster <guido+sievemgr@berhoerster.name> |
---|---|
date | Tue, 03 Nov 2020 23:44:45 +0100 |
parents | 0cd5a454dfb4 |
children | 29769b9e2f09 |
comparison
equal
deleted
inserted
replaced
4:f925f15d8ce5 | 5:4dff4c3f0fbb |
---|---|
20 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | 20 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
21 | 21 |
22 package main | 22 package main |
23 | 23 |
24 import ( | 24 import ( |
25 "bufio" | |
26 "crypto/tls" | 25 "crypto/tls" |
27 "errors" | 26 "errors" |
28 "fmt" | 27 "fmt" |
29 "io" | 28 "io" |
30 "io/ioutil" | 29 "io/ioutil" |
31 "net" | 30 "net" |
32 "os" | 31 "os" |
33 "os/user" | |
34 "runtime" | 32 "runtime" |
35 "strings" | 33 "strings" |
36 | 34 |
37 "go.guido-berhoerster.org/managesieve" | 35 "go.guido-berhoerster.org/managesieve" |
36 "go.guido-berhoerster.org/sievemgr/cmd/sievemgr/internal/config" | |
38 "golang.org/x/crypto/ssh/terminal" | 37 "golang.org/x/crypto/ssh/terminal" |
39 ) | 38 ) |
40 | 39 |
41 var errTooBig = errors.New("too big") | 40 var errTooBig = errors.New("too big") |
42 | 41 |
43 func parseHostPort(s string) (string, string, error) { | 42 func getAccount(conf *config.Configuration, name string) (*config.Account, error) { |
44 var host string | 43 if name == "" { |
45 host, port, err := net.SplitHostPort(s) | 44 if conf.Default == nil { |
46 if err != nil { | 45 return nil, fmt.Errorf("no default account configured") |
47 // error may be due to a missing port but there is no usable | 46 } |
48 // error value to test for, thus try again with a port added | 47 return conf.Default, nil |
49 var tmpErr error | 48 } |
50 host, _, tmpErr = net.SplitHostPort(s + ":4190") | 49 for _, acct := range conf.Accounts { |
51 if tmpErr != nil { | 50 if acct.Name == name { |
52 return "", "", err | 51 return acct, nil |
53 } | 52 } |
54 } | 53 } |
55 if port == "" { | 54 return nil, fmt.Errorf("account %q does not exist", name) |
56 // no port given, try to look up a SRV record for given domain | |
57 // and fall back to the domain and port 4190 | |
58 services, err := managesieve.LookupService(host) | |
59 if err != nil { | |
60 return "", "", err | |
61 } | |
62 host, port, err = net.SplitHostPort(services[0]) | |
63 if err != nil { | |
64 return "", "", err | |
65 } | |
66 } | |
67 return host, port, nil | |
68 } | 55 } |
69 | 56 |
70 func readPassword() (string, error) { | 57 func readPassword(acct *config.Account) error { |
58 if acct.Password != "" { | |
59 return nil | |
60 } | |
61 | |
71 var tty *os.File | 62 var tty *os.File |
72 var fd int | 63 var fd int |
73 var w io.Writer | 64 var w io.Writer |
74 if runtime.GOOS == "windows" { | 65 if runtime.GOOS == "windows" { |
75 fd = int(os.Stdin.Fd()) | 66 fd = int(os.Stdin.Fd()) |
76 w = os.Stdout | 67 w = os.Stdout |
77 } else { | 68 } else { |
78 var err error | 69 var err error |
79 tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0666) | 70 tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0666) |
80 if err != nil { | 71 if err != nil { |
81 return "", err | 72 return err |
82 } | 73 } |
83 defer tty.Close() | 74 defer tty.Close() |
84 fd = int(tty.Fd()) | 75 fd = int(tty.Fd()) |
85 w = tty | 76 w = tty |
86 } | 77 } |
87 | 78 |
88 io.WriteString(w, "Password: ") | 79 io.WriteString(w, "Password: ") |
89 rawPassword, err := terminal.ReadPassword(fd) | 80 rawPassword, err := terminal.ReadPassword(fd) |
90 io.WriteString(w, "\n") | 81 io.WriteString(w, "\n") |
91 if err != nil { | 82 if err != nil { |
92 return "", fmt.Errorf("failed to read password: %s", err) | 83 return fmt.Errorf("failed to read password: %s", err) |
93 } | 84 } |
94 password := string(rawPassword) | 85 password := string(rawPassword) |
95 if password == "" { | 86 if password == "" { |
96 return "", fmt.Errorf("invalid password") | 87 return fmt.Errorf("invalid password") |
97 } | 88 } |
98 return password, nil | 89 acct.Password = password |
90 return nil | |
99 } | 91 } |
100 | 92 |
101 func readPasswordFile(filename string) (string, error) { | 93 func lookupHostPort(acct *config.Account) error { |
102 f, err := os.Open(filename) | 94 if acct.Port != "" { |
95 return nil | |
96 } | |
97 // no port given, try to look up a SRV record for given domain | |
98 // and fall back to the domain and port 4190 | |
99 services, err := managesieve.LookupService(acct.Host) | |
103 if err != nil { | 100 if err != nil { |
104 return "", err | 101 return fmt.Errorf("failed to look up service record: %s", err) |
105 } | 102 } |
106 defer f.Close() | 103 host, port, err := net.SplitHostPort(services[0]) |
107 scanner := bufio.NewScanner(f) | 104 if err != nil { |
108 if !scanner.Scan() { | 105 return fmt.Errorf("failed to parse service record: %s", err) |
109 err := scanner.Err() | |
110 if err == nil { | |
111 err = fmt.Errorf("failed to read from %q: unexpected EOF", | |
112 filename) | |
113 } | |
114 return "", err | |
115 } | 106 } |
116 password := scanner.Text() | 107 acct.Host = host |
117 if password == "" { | 108 acct.Port = port |
118 return "", fmt.Errorf("invalid password") | 109 return nil |
119 } | |
120 return password, nil | |
121 } | 110 } |
122 | 111 |
123 func usernamePassword(host, port, username, passwordFile string) (string, string, error) { | 112 func dialPlainAuth(acct *config.Account) (*managesieve.Client, error) { |
124 // fall back to the system username | 113 c, err := managesieve.Dial(net.JoinHostPort(acct.Host, acct.Port)) |
125 if username == "" { | |
126 u, err := user.Current() | |
127 if err != nil { | |
128 return "", "", | |
129 fmt.Errorf("failed to obtain username: %s", err) | |
130 } | |
131 username = u.Username | |
132 } | |
133 | |
134 var password string | |
135 var err error | |
136 if passwordFile != "" { | |
137 password, err = readPasswordFile(passwordFilename) | |
138 } else { | |
139 password, err = readPassword() | |
140 } | |
141 if err != nil { | |
142 return "", "", err | |
143 } | |
144 | |
145 return username, password, nil | |
146 } | |
147 | |
148 func dialPlainAuth(hostport, username, password string) (*managesieve.Client, error) { | |
149 c, err := managesieve.Dial(hostport) | |
150 if err != nil { | 114 if err != nil { |
151 return nil, fmt.Errorf("failed to connect: %s", err) | 115 return nil, fmt.Errorf("failed to connect: %s", err) |
152 } | 116 } |
153 | 117 |
154 host, _, _ := net.SplitHostPort(hostport) | |
155 // switch to a TLS connection except for localhost | 118 // switch to a TLS connection except for localhost |
156 if host != "localhost" && host != "127.0.0.1" && host != "::1" { | 119 if acct.Host != "localhost" && acct.Host != "127.0.0.1" && |
120 acct.Host != "::1" { | |
157 tlsConf := &tls.Config{ | 121 tlsConf := &tls.Config{ |
158 ServerName: host, | 122 ServerName: acct.Host, |
159 InsecureSkipVerify: skipCertVerify, | 123 InsecureSkipVerify: acct.Insecure, |
160 } | 124 } |
161 if err := c.StartTLS(tlsConf); err != nil { | 125 if err := c.StartTLS(tlsConf); err != nil { |
162 return nil, | 126 return nil, |
163 fmt.Errorf("failed to start TLS connection: %s", | 127 fmt.Errorf("failed to start TLS connection: %s", |
164 err) | 128 err) |
165 } | 129 } |
166 } | 130 } |
167 | 131 |
168 auth := managesieve.PlainAuth("", username, password, host) | 132 auth := managesieve.PlainAuth("", acct.User, acct.Password, acct.Host) |
169 if err := c.Authenticate(auth); err != nil { | 133 if err := c.Authenticate(auth); err != nil { |
170 return nil, fmt.Errorf("failed to authenticate user %s: %s", | 134 return nil, fmt.Errorf("failed to authenticate user %s: %s", |
171 username, err) | 135 acct.User, err) |
172 } | 136 } |
173 | 137 |
174 return c, nil | 138 return c, nil |
175 } | 139 } |
176 | 140 |