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