source: code/trunk/service.go@ 307

Last change on this file since 307 was 307, checked in by fox.cpp, 5 years ago

Implement upstream SASL EXTERNAL support

Closes: https://todo.sr.ht/~emersion/soju/47

File size: 11.6 KB
Line 
1package soju
2
3import (
4 "crypto"
5 "crypto/ecdsa"
6 "crypto/ed25519"
7 "crypto/elliptic"
8 "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha1"
11 "crypto/sha256"
12 "crypto/x509"
13 "crypto/x509/pkix"
14 "encoding/hex"
15 "errors"
16 "flag"
17 "fmt"
18 "io/ioutil"
19 "math/big"
20 "strings"
21 "time"
22
23 "github.com/google/shlex"
24 "golang.org/x/crypto/bcrypt"
25 "gopkg.in/irc.v3"
26)
27
28const serviceNick = "BouncerServ"
29
30var servicePrefix = &irc.Prefix{
31 Name: serviceNick,
32 User: serviceNick,
33 Host: serviceNick,
34}
35
36type serviceCommandSet map[string]*serviceCommand
37
38type serviceCommand struct {
39 usage string
40 desc string
41 handle func(dc *downstreamConn, params []string) error
42 children serviceCommandSet
43}
44
45func sendServiceNOTICE(dc *downstreamConn, text string) {
46 dc.SendMessage(&irc.Message{
47 Prefix: servicePrefix,
48 Command: "NOTICE",
49 Params: []string{dc.nick, text},
50 })
51}
52
53func sendServicePRIVMSG(dc *downstreamConn, text string) {
54 dc.SendMessage(&irc.Message{
55 Prefix: servicePrefix,
56 Command: "PRIVMSG",
57 Params: []string{dc.nick, text},
58 })
59}
60
61func handleServicePRIVMSG(dc *downstreamConn, text string) {
62 words, err := shlex.Split(text)
63 if err != nil {
64 sendServicePRIVMSG(dc, fmt.Sprintf("error: failed to parse command: %v", err))
65 return
66 }
67
68 cmd, params, err := serviceCommands.Get(words)
69 if err != nil {
70 sendServicePRIVMSG(dc, fmt.Sprintf(`error: %v (type "help" for a list of commands)`, err))
71 return
72 }
73
74 if err := cmd.handle(dc, params); err != nil {
75 sendServicePRIVMSG(dc, fmt.Sprintf("error: %v", err))
76 }
77}
78
79func (cmds serviceCommandSet) Get(params []string) (*serviceCommand, []string, error) {
80 if len(params) == 0 {
81 return nil, nil, fmt.Errorf("no command specified")
82 }
83
84 name := params[0]
85 params = params[1:]
86
87 cmd, ok := cmds[name]
88 if !ok {
89 for k := range cmds {
90 if !strings.HasPrefix(k, name) {
91 continue
92 }
93 if cmd != nil {
94 return nil, params, fmt.Errorf("command %q is ambiguous", name)
95 }
96 cmd = cmds[k]
97 }
98 }
99 if cmd == nil {
100 return nil, params, fmt.Errorf("command %q not found", name)
101 }
102
103 if len(params) == 0 || len(cmd.children) == 0 {
104 return cmd, params, nil
105 }
106 return cmd.children.Get(params)
107}
108
109var serviceCommands serviceCommandSet
110
111func init() {
112 serviceCommands = serviceCommandSet{
113 "help": {
114 usage: "[command]",
115 desc: "print help message",
116 handle: handleServiceHelp,
117 },
118 "network": {
119 children: serviceCommandSet{
120 "create": {
121 usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [[-connect-command command] ...]",
122 desc: "add a new network",
123 handle: handleServiceCreateNetwork,
124 },
125 "status": {
126 desc: "show a list of saved networks and their current status",
127 handle: handleServiceNetworkStatus,
128 },
129 "delete": {
130 usage: "<name>",
131 desc: "delete a network",
132 handle: handleServiceNetworkDelete,
133 },
134 },
135 },
136 "certfp": {
137 children: serviceCommandSet{
138 "generate": {
139 usage: "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
140 desc: "generate a new self-signed certificate, defaults to using RSA-3072 key",
141 handle: handleServiceCertfpGenerate,
142 },
143 "fingerprint": {
144 usage: "<network name>",
145 desc: "show fingerprints of certificate associated with the network",
146 handle: handleServiceCertfpFingerprints,
147 },
148 "reset": {
149 usage: "<network name>",
150 desc: "disable SASL EXTERNAL authentication and remove stored certificate",
151 handle: handleServiceCertfpReset,
152 },
153 },
154 },
155 "change-password": {
156 usage: "<new password>",
157 desc: "change your password",
158 handle: handlePasswordChange,
159 },
160 }
161}
162
163func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
164 fs := newFlagSet()
165 keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
166 bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
167
168 if err := fs.Parse(params); err != nil {
169 return err
170 }
171
172 if len(fs.Args()) != 1 {
173 return errors.New("exactly one argument is required")
174 }
175
176 net := dc.user.getNetwork(fs.Arg(0))
177 if net == nil {
178 return fmt.Errorf("unknown network %q", fs.Arg(0))
179 }
180
181 var (
182 privKey crypto.PrivateKey
183 pubKey crypto.PublicKey
184 )
185 switch *keyType {
186 case "rsa":
187 key, err := rsa.GenerateKey(rand.Reader, *bits)
188 if err != nil {
189 return err
190 }
191 privKey = key
192 pubKey = key.Public()
193 case "ecdsa":
194 key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
195 if err != nil {
196 return err
197 }
198 privKey = key
199 pubKey = key.Public()
200 case "ed25519":
201 var err error
202 pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
203 if err != nil {
204 return err
205 }
206 }
207
208 // Using PKCS#8 allows easier extension for new key types.
209 privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
210 if err != nil {
211 return err
212 }
213
214 notBefore := time.Now()
215 // Lets make a fair assumption nobody will use the same cert for more than 20 years...
216 notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
217 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
218 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
219 if err != nil {
220 return err
221 }
222 cert := &x509.Certificate{
223 SerialNumber: serialNumber,
224 Subject: pkix.Name{CommonName: "soju auto-generated certificate"},
225 NotBefore: notBefore,
226 NotAfter: notAfter,
227 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
228 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
229 }
230 derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
231 if err != nil {
232 return err
233 }
234
235 net.SASL.External.CertBlob = derBytes
236 net.SASL.External.PrivKeyBlob = privKeyBytes
237 net.SASL.Mechanism = "EXTERNAL"
238
239 if err := dc.srv.db.StoreNetwork(net.Username, &net.Network); err != nil {
240 return err
241 }
242
243 sendServicePRIVMSG(dc, "certificate generated")
244
245 sha1Sum := sha1.Sum(derBytes)
246 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
247 sha256Sum := sha256.Sum256(derBytes)
248 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
249
250 return nil
251}
252
253func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
254 if len(params) != 1 {
255 return fmt.Errorf("expected exactly one argument")
256 }
257
258 net := dc.user.getNetwork(params[0])
259 if net == nil {
260 return fmt.Errorf("unknown network %q", params[0])
261 }
262
263 sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
264 sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
265 sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
266 sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
267 return nil
268}
269
270func handleServiceCertfpReset(dc *downstreamConn, params []string) error {
271 if len(params) != 1 {
272 return fmt.Errorf("expected exactly one argument")
273 }
274
275 net := dc.user.getNetwork(params[0])
276 if net == nil {
277 return fmt.Errorf("unknown network %q", params[0])
278 }
279
280 net.SASL.External.CertBlob = nil
281 net.SASL.External.PrivKeyBlob = nil
282
283 if net.SASL.Mechanism == "EXTERNAL" {
284 net.SASL.Mechanism = ""
285 }
286 if err := dc.srv.db.StoreNetwork(dc.user.Username, &net.Network); err != nil {
287 return err
288 }
289
290 sendServicePRIVMSG(dc, "certificate reset")
291 return nil
292}
293
294func appendServiceCommandSetHelp(cmds serviceCommandSet, prefix []string, l *[]string) {
295 for name, cmd := range cmds {
296 words := append(prefix, name)
297 if len(cmd.children) == 0 {
298 s := strings.Join(words, " ")
299 *l = append(*l, s)
300 } else {
301 appendServiceCommandSetHelp(cmd.children, words, l)
302 }
303 }
304}
305
306func handleServiceHelp(dc *downstreamConn, params []string) error {
307 if len(params) > 0 {
308 cmd, rest, err := serviceCommands.Get(params)
309 if err != nil {
310 return err
311 }
312 words := params[:len(params)-len(rest)]
313
314 if len(cmd.children) > 0 {
315 var l []string
316 appendServiceCommandSetHelp(cmd.children, words, &l)
317 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
318 } else {
319 text := strings.Join(words, " ")
320 if cmd.usage != "" {
321 text += " " + cmd.usage
322 }
323 text += ": " + cmd.desc
324
325 sendServicePRIVMSG(dc, text)
326 }
327 } else {
328 var l []string
329 appendServiceCommandSetHelp(serviceCommands, nil, &l)
330 sendServicePRIVMSG(dc, "available commands: "+strings.Join(l, ", "))
331 }
332 return nil
333}
334
335func newFlagSet() *flag.FlagSet {
336 fs := flag.NewFlagSet("", flag.ContinueOnError)
337 fs.SetOutput(ioutil.Discard)
338 return fs
339}
340
341type stringSliceVar []string
342
343func (v *stringSliceVar) String() string {
344 return fmt.Sprint([]string(*v))
345}
346
347func (v *stringSliceVar) Set(s string) error {
348 *v = append(*v, s)
349 return nil
350}
351
352func handleServiceCreateNetwork(dc *downstreamConn, params []string) error {
353 fs := newFlagSet()
354 addr := fs.String("addr", "", "")
355 name := fs.String("name", "", "")
356 username := fs.String("username", "", "")
357 pass := fs.String("pass", "", "")
358 realname := fs.String("realname", "", "")
359 nick := fs.String("nick", "", "")
360 var connectCommands stringSliceVar
361 fs.Var(&connectCommands, "connect-command", "")
362
363 if err := fs.Parse(params); err != nil {
364 return err
365 }
366 if *addr == "" {
367 return fmt.Errorf("flag -addr is required")
368 }
369
370 if addrParts := strings.SplitN(*addr, "://", 2); len(addrParts) == 2 {
371 scheme := addrParts[0]
372 switch scheme {
373 case "ircs", "irc+insecure":
374 default:
375 return fmt.Errorf("unknown scheme %q (supported schemes: ircs, irc+insecure)", scheme)
376 }
377 }
378
379 for _, command := range connectCommands {
380 _, err := irc.ParseMessage(command)
381 if err != nil {
382 return fmt.Errorf("flag -connect-command must be a valid raw irc command string: %q: %v", command, err)
383 }
384 }
385
386 if *nick == "" {
387 *nick = dc.nick
388 }
389
390 var err error
391 network, err := dc.user.createNetwork(&Network{
392 Addr: *addr,
393 Name: *name,
394 Username: *username,
395 Pass: *pass,
396 Realname: *realname,
397 Nick: *nick,
398 ConnectCommands: connectCommands,
399 })
400 if err != nil {
401 return fmt.Errorf("could not create network: %v", err)
402 }
403
404 sendServicePRIVMSG(dc, fmt.Sprintf("created network %q", network.GetName()))
405 return nil
406}
407
408func handleServiceNetworkStatus(dc *downstreamConn, params []string) error {
409 dc.user.forEachNetwork(func(net *network) {
410 var statuses []string
411 var details string
412 if uc := net.conn; uc != nil {
413 if dc.nick != uc.nick {
414 statuses = append(statuses, "connected as "+uc.nick)
415 } else {
416 statuses = append(statuses, "connected")
417 }
418 details = fmt.Sprintf("%v channels", len(uc.channels))
419 } else {
420 statuses = append(statuses, "disconnected")
421 if net.lastError != nil {
422 details = net.lastError.Error()
423 }
424 }
425
426 if net == dc.network {
427 statuses = append(statuses, "current")
428 }
429
430 name := net.GetName()
431 if name != net.Addr {
432 name = fmt.Sprintf("%v (%v)", name, net.Addr)
433 }
434
435 s := fmt.Sprintf("%v [%v]", name, strings.Join(statuses, ", "))
436 if details != "" {
437 s += ": " + details
438 }
439 sendServicePRIVMSG(dc, s)
440 })
441 return nil
442}
443
444func handleServiceNetworkDelete(dc *downstreamConn, params []string) error {
445 if len(params) != 1 {
446 return fmt.Errorf("expected exactly one argument")
447 }
448
449 net := dc.user.getNetwork(params[0])
450 if net == nil {
451 return fmt.Errorf("unknown network %q", params[0])
452 }
453
454 if err := dc.user.deleteNetwork(net.ID); err != nil {
455 return err
456 }
457
458 sendServicePRIVMSG(dc, fmt.Sprintf("deleted network %q", net.GetName()))
459 return nil
460}
461
462func handlePasswordChange(dc *downstreamConn, params []string) error {
463 if len(params) != 1 {
464 return fmt.Errorf("expected exactly one argument")
465 }
466
467 hashed, err := bcrypt.GenerateFromPassword([]byte(params[0]), bcrypt.DefaultCost)
468 if err != nil {
469 return fmt.Errorf("failed to hash password: %v", err)
470 }
471 if err := dc.user.updatePassword(string(hashed)); err != nil {
472 return err
473 }
474
475 sendServicePRIVMSG(dc, "password updated")
476 return nil
477}
Note: See TracBrowser for help on using the repository browser.