Browse Source

basic capability negotiation

- multi-prefix is supported as an example
tags/v0.1.0
Jeremy Latt 10 years ago
parent
commit
36602c9a3c
7 changed files with 206 additions and 100 deletions
  1. 16
    8
      irc/channel.go
  2. 36
    33
      irc/client.go
  3. 21
    6
      irc/commands.go
  4. 30
    3
      irc/constants.go
  5. 19
    6
      irc/reply.go
  6. 63
    42
      irc/server.go
  7. 21
    2
      irc/types.go

+ 16
- 8
irc/channel.go View File

@@ -54,18 +54,26 @@ func (channel *Channel) ClientIsOperator(client *Client) bool {
54 54
 	return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator)
55 55
 }
56 56
 
57
-func (channel *Channel) Nicks() []string {
57
+func (channel *Channel) Nicks(target *Client) []string {
58
+	isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix]
58 59
 	nicks := make([]string, len(channel.members))
59 60
 	i := 0
60 61
 	for client, modes := range channel.members {
61
-		switch {
62
-		case modes[ChannelOperator]:
63
-			nicks[i] = "@" + client.Nick()
64
-		case modes[Voice]:
65
-			nicks[i] = "+" + client.Nick()
66
-		default:
67
-			nicks[i] = client.Nick()
62
+		if isMultiPrefix {
63
+			if modes[ChannelOperator] {
64
+				nicks[i] += "@"
65
+			}
66
+			if modes[Voice] {
67
+				nicks[i] += "+"
68
+			}
69
+		} else {
70
+			if modes[ChannelOperator] {
71
+				nicks[i] += "@"
72
+			} else if modes[Voice] {
73
+				nicks[i] += "+"
74
+			}
68 75
 		}
76
+		nicks[i] += client.Nick()
69 77
 		i += 1
70 78
 	}
71 79
 	return nicks

+ 36
- 33
irc/client.go View File

@@ -12,36 +12,41 @@ func IsNickname(nick string) bool {
12 12
 }
13 13
 
14 14
 type Client struct {
15
-	atime       time.Time
16
-	awayMessage string
17
-	channels    ChannelSet
18
-	commands    chan editableCommand
19
-	ctime       time.Time
20
-	flags       map[UserMode]bool
21
-	hasQuit     bool
22
-	hops        uint
23
-	hostname    string
24
-	idleTimer   *time.Timer
25
-	loginTimer  *time.Timer
26
-	nick        string
27
-	phase       Phase
28
-	quitTimer   *time.Timer
29
-	realname    string
30
-	server      *Server
31
-	socket      *Socket
32
-	username    string
15
+	atime        time.Time
16
+	authorized   bool
17
+	awayMessage  string
18
+	capabilities CapabilitySet
19
+	capState     CapState
20
+	channels     ChannelSet
21
+	commands     chan editableCommand
22
+	ctime        time.Time
23
+	flags        map[UserMode]bool
24
+	hasQuit      bool
25
+	hops         uint
26
+	hostname     string
27
+	idleTimer    *time.Timer
28
+	loginTimer   *time.Timer
29
+	nick         string
30
+	phase        Phase
31
+	quitTimer    *time.Timer
32
+	realname     string
33
+	server       *Server
34
+	socket       *Socket
35
+	username     string
33 36
 }
34 37
 
35 38
 func NewClient(server *Server, conn net.Conn) *Client {
36 39
 	now := time.Now()
37 40
 	client := &Client{
38
-		atime:    now,
39
-		channels: make(ChannelSet),
40
-		commands: make(chan editableCommand),
41
-		ctime:    now,
42
-		flags:    make(map[UserMode]bool),
43
-		phase:    server.InitPhase(),
44
-		server:   server,
41
+		atime:        now,
42
+		capState:     CapNone,
43
+		capabilities: make(CapabilitySet),
44
+		channels:     make(ChannelSet),
45
+		commands:     make(chan editableCommand),
46
+		ctime:        now,
47
+		flags:        make(map[UserMode]bool),
48
+		phase:        Registration,
49
+		server:       server,
45 50
 	}
46 51
 	client.socket = NewSocket(conn, client.commands)
47 52
 	client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout)
@@ -68,6 +73,12 @@ func (client *Client) run() {
68 73
 	}
69 74
 }
70 75
 
76
+func (client *Client) connectionTimeout() {
77
+	client.commands <- &QuitCommand{
78
+		message: "connection timeout",
79
+	}
80
+}
81
+
71 82
 //
72 83
 // idle timer goroutine
73 84
 //
@@ -76,14 +87,6 @@ func (client *Client) connectionIdle() {
76 87
 	client.server.idle <- client
77 88
 }
78 89
 
79
-//
80
-// quit timer goroutine
81
-//
82
-
83
-func (client *Client) connectionTimeout() {
84
-	client.server.timeout <- client
85
-}
86
-
87 90
 //
88 91
 // server goroutine
89 92
 //

+ 21
- 6
irc/commands.go View File

@@ -706,20 +706,35 @@ func NewOperCommand(args []string) (editableCommand, error) {
706 706
 	return cmd, nil
707 707
 }
708 708
 
709
-// TODO
710 709
 type CapCommand struct {
711 710
 	BaseCommand
712
-	args []string
711
+	subCommand CapSubCommand
712
+	args       []string
713 713
 }
714 714
 
715 715
 func (msg *CapCommand) String() string {
716
-	return fmt.Sprintf("CAP(args=%s)", msg.args)
716
+	return fmt.Sprintf("CAP(subCommand=%s, args=%s)", msg.subCommand, msg.args)
717
+}
718
+
719
+func (msg *CapCommand) Capabilities() []Capability {
720
+	strs := strings.Split(msg.args[0], " ")
721
+	caps := make([]Capability, len(strs))
722
+	for index, str := range strs {
723
+		caps[index] = Capability(str)
724
+	}
725
+	return caps
717 726
 }
718 727
 
719 728
 func NewCapCommand(args []string) (editableCommand, error) {
720
-	return &CapCommand{
721
-		args: args,
722
-	}, nil
729
+	if len(args) < 1 {
730
+		return nil, NotEnoughArgsError
731
+	}
732
+
733
+	cmd := &CapCommand{
734
+		subCommand: CapSubCommand(args[0]),
735
+		args:       args[1:],
736
+	}
737
+	return cmd, nil
723 738
 }
724 739
 
725 740
 // HAPROXY support

+ 30
- 3
irc/constants.go View File

@@ -155,6 +155,7 @@ const (
155 155
 	ERR_TOOMANYTARGETS    NumericCode = 407
156 156
 	ERR_NOSUCHSERVICE     NumericCode = 408
157 157
 	ERR_NOORIGIN          NumericCode = 409
158
+	ERR_INVALIDCAPCMD     NumericCode = 410
158 159
 	ERR_NORECIPIENT       NumericCode = 411
159 160
 	ERR_NOTEXTTOSEND      NumericCode = 412
160 161
 	ERR_NOTOPLEVEL        NumericCode = 413
@@ -200,6 +201,14 @@ const (
200 201
 	ERR_UMODEUNKNOWNFLAG  NumericCode = 501
201 202
 	ERR_USERSDONTMATCH    NumericCode = 502
202 203
 
204
+	CAP_LS    CapSubCommand = "LS"
205
+	CAP_LIST  CapSubCommand = "LIST"
206
+	CAP_REQ   CapSubCommand = "REQ"
207
+	CAP_ACK   CapSubCommand = "ACK"
208
+	CAP_NAK   CapSubCommand = "NAK"
209
+	CAP_CLEAR CapSubCommand = "CLEAR"
210
+	CAP_END   CapSubCommand = "END"
211
+
203 212
 	Add    ModeOp = '+'
204 213
 	List   ModeOp = '='
205 214
 	Remove ModeOp = '-'
@@ -230,10 +239,28 @@ const (
230 239
 	Secret          ChannelMode = 's' // flag, deprecated
231 240
 	UserLimit       ChannelMode = 'l' // flag arg
232 241
 	Voice           ChannelMode = 'v' // arg
242
+
243
+	MultiPrefix Capability = "multi-prefix"
244
+	SASL        Capability = "sasl"
245
+
246
+	Disable CapModifier = '-'
247
+	Ack     CapModifier = '~'
248
+	Sticky  CapModifier = '='
249
+)
250
+
251
+var (
252
+	SupportedCapabilities = CapabilitySet{
253
+		MultiPrefix: true,
254
+	}
255
+)
256
+
257
+const (
258
+	Registration Phase = iota
259
+	Normal       Phase = iota
233 260
 )
234 261
 
235 262
 const (
236
-	Authorization Phase = iota
237
-	Registration  Phase = iota
238
-	Normal        Phase = iota
263
+	CapNone        CapState = iota
264
+	CapNegotiating CapState = iota
265
+	CapNegotiated  CapState = iota
239 266
 )

+ 19
- 6
irc/reply.go View File

@@ -240,11 +240,19 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) {
240 240
 
241 241
 	if channel != nil {
242 242
 		channelName = channel.name
243
-
244
-		if channel.members[client][ChannelOperator] {
245
-			flags += "@"
246
-		} else if channel.members[client][Voice] {
247
-			flags += "+"
243
+		if target.capabilities[MultiPrefix] {
244
+			if channel.members[client][ChannelOperator] {
245
+				flags += "@"
246
+			}
247
+			if channel.members[client][Voice] {
248
+				flags += "+"
249
+			}
250
+		} else {
251
+			if channel.members[client][ChannelOperator] {
252
+				flags += "@"
253
+			} else if channel.members[client][Voice] {
254
+				flags += "+"
255
+			}
248 256
 		}
249 257
 	}
250 258
 	target.NumericReply(RPL_WHOREPLY,
@@ -360,7 +368,7 @@ func (target *Client) RplListEnd(server *Server) {
360 368
 }
361 369
 
362 370
 func (target *Client) RplNamReply(channel *Channel) {
363
-	target.MultilineReply(channel.Nicks(), RPL_NAMREPLY,
371
+	target.MultilineReply(channel.Nicks(target), RPL_NAMREPLY,
364 372
 		"= %s :%s", channel)
365 373
 }
366 374
 
@@ -502,3 +510,8 @@ func (target *Client) ErrChannelIsFull(channel *Channel) {
502 510
 	target.NumericReply(ERR_CHANNELISFULL,
503 511
 		"%s :Cannot join channel (+l)", channel)
504 512
 }
513
+
514
+func (target *Client) ErrInvalidCapCmd(subCommand CapSubCommand) {
515
+	target.NumericReply(ERR_INVALIDCAPCMD,
516
+		"%s :Invalid CAP subcommand", subCommand)
517
+}

+ 63
- 42
irc/server.go View File

@@ -32,7 +32,6 @@ type Server struct {
32 32
 	operators map[string][]byte
33 33
 	password  []byte
34 34
 	signals   chan os.Signal
35
-	timeout   chan *Client
36 35
 }
37 36
 
38 37
 func NewServer(config *Config) *Server {
@@ -54,7 +53,6 @@ func NewServer(config *Config) *Server {
54 53
 		operators: config.OperatorsMap(),
55 54
 		password:  config.PasswordBytes(),
56 55
 		signals:   make(chan os.Signal, 1),
57
-		timeout:   make(chan *Client, 16),
58 56
 	}
59 57
 
60 58
 	signal.Notify(server.signals, os.Interrupt, os.Kill)
@@ -101,14 +99,6 @@ func (server *Server) processCommand(cmd Command) {
101 99
 	}
102 100
 
103 101
 	switch client.phase {
104
-	case Authorization:
105
-		authCmd, ok := cmd.(AuthServerCommand)
106
-		if !ok {
107
-			client.Quit("unexpected command")
108
-			return
109
-		}
110
-		authCmd.HandleAuthServer(server)
111
-
112 102
 	case Registration:
113 103
 		regCmd, ok := cmd.(RegServerCommand)
114 104
 		if !ok {
@@ -117,7 +107,7 @@ func (server *Server) processCommand(cmd Command) {
117 107
 		}
118 108
 		regCmd.HandleRegServer(server)
119 109
 
120
-	default:
110
+	case Normal:
121 111
 		srvCmd, ok := cmd.(ServerCommand)
122 112
 		if !ok {
123 113
 			client.ErrUnknownCommand(cmd.Code())
@@ -155,20 +145,10 @@ func (server *Server) Run() {
155 145
 
156 146
 		case client := <-server.idle:
157 147
 			client.Idle()
158
-
159
-		case client := <-server.timeout:
160
-			client.Quit("connection timeout")
161 148
 		}
162 149
 	}
163 150
 }
164 151
 
165
-func (server *Server) InitPhase() Phase {
166
-	if server.password == nil {
167
-		return Registration
168
-	}
169
-	return Authorization
170
-}
171
-
172 152
 func newListener(config ListenerConfig) (net.Listener, error) {
173 153
 	if config.IsTLS() {
174 154
 		certificate, err := tls.LoadX509KeyPair(config.Certificate, config.Key)
@@ -237,7 +217,7 @@ func (s *Server) GenerateGuestNick() string {
237 217
 //
238 218
 
239 219
 func (s *Server) tryRegister(c *Client) {
240
-	if c.HasNick() && c.HasUsername() {
220
+	if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) {
241 221
 		c.Register()
242 222
 		c.RplWelcome()
243 223
 		c.RplYourHost()
@@ -297,18 +277,10 @@ func (s *Server) Nick() string {
297 277
 }
298 278
 
299 279
 //
300
-// authorization commands
280
+// registration commands
301 281
 //
302 282
 
303
-func (msg *ProxyCommand) HandleAuthServer(server *Server) {
304
-	msg.Client().hostname = msg.hostname
305
-}
306
-
307
-func (msg *CapCommand) HandleAuthServer(server *Server) {
308
-	// TODO
309
-}
310
-
311
-func (msg *PassCommand) HandleAuthServer(server *Server) {
283
+func (msg *PassCommand) HandleRegServer(server *Server) {
312 284
 	client := msg.Client()
313 285
 	if msg.err != nil {
314 286
 		client.ErrPasswdMismatch()
@@ -316,27 +288,62 @@ func (msg *PassCommand) HandleAuthServer(server *Server) {
316 288
 		return
317 289
 	}
318 290
 
319
-	client.phase = Registration
291
+	client.authorized = true
320 292
 }
321 293
 
322
-func (msg *QuitCommand) HandleAuthServer(server *Server) {
323
-	msg.Client().Quit(msg.message)
324
-}
325
-
326
-//
327
-// registration commands
328
-//
329
-
330 294
 func (msg *ProxyCommand) HandleRegServer(server *Server) {
331 295
 	msg.Client().hostname = msg.hostname
332 296
 }
333 297
 
334 298
 func (msg *CapCommand) HandleRegServer(server *Server) {
335
-	// TODO
299
+	client := msg.Client()
300
+
301
+	switch msg.subCommand {
302
+	case CAP_LS:
303
+		client.capState = CapNegotiating
304
+		client.Reply(fmt.Sprintf("CAP LS :%d", MultiPrefix))
305
+
306
+	case CAP_LIST:
307
+		client.Reply(fmt.Sprintf("CAP LIST :%s", client.capabilities))
308
+
309
+	case CAP_REQ:
310
+		client.capState = CapNegotiating
311
+		caps := msg.Capabilities()
312
+		if (len(caps) != 1) && (caps[0] != MultiPrefix) {
313
+			client.Reply("CAP NAK :" + msg.args[0])
314
+			return
315
+		}
316
+		for _, capability := range caps {
317
+			client.capabilities[capability] = true
318
+		}
319
+		client.Reply("CAP ACK :" + msg.args[0])
320
+
321
+	case CAP_CLEAR:
322
+		for capability := range client.capabilities {
323
+			delete(client.capabilities, capability)
324
+		}
325
+		client.Reply("CAP ACK :")
326
+
327
+	case CAP_END:
328
+		client.capState = CapNegotiated
329
+		server.tryRegister(client)
330
+
331
+	default:
332
+		client.ErrInvalidCapCmd(msg.subCommand)
333
+	}
336 334
 }
337 335
 
338 336
 func (m *NickCommand) HandleRegServer(s *Server) {
339 337
 	client := m.Client()
338
+	if !client.authorized {
339
+		client.ErrPasswdMismatch()
340
+		client.Quit("bad password")
341
+		return
342
+	}
343
+
344
+	if client.capState == CapNegotiating {
345
+		client.capState = CapNegotiated
346
+	}
340 347
 
341 348
 	if m.nickname == "" {
342 349
 		client.ErrNoNicknameGiven()
@@ -358,11 +365,22 @@ func (m *NickCommand) HandleRegServer(s *Server) {
358 365
 }
359 366
 
360 367
 func (msg *RFC1459UserCommand) HandleRegServer(server *Server) {
368
+	client := msg.Client()
369
+	if !client.authorized {
370
+		client.ErrPasswdMismatch()
371
+		client.Quit("bad password")
372
+		return
373
+	}
361 374
 	msg.HandleRegServer2(server)
362 375
 }
363 376
 
364 377
 func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
365 378
 	client := msg.Client()
379
+	if !client.authorized {
380
+		client.ErrPasswdMismatch()
381
+		client.Quit("bad password")
382
+		return
383
+	}
366 384
 	flags := msg.Flags()
367 385
 	if len(flags) > 0 {
368 386
 		for _, mode := range msg.Flags() {
@@ -375,6 +393,9 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
375 393
 
376 394
 func (msg *UserCommand) HandleRegServer2(server *Server) {
377 395
 	client := msg.Client()
396
+	if client.capState == CapNegotiating {
397
+		client.capState = CapNegotiated
398
+	}
378 399
 	client.username, client.realname = msg.username, msg.realname
379 400
 	server.tryRegister(client)
380 401
 }

+ 21
- 2
irc/types.go View File

@@ -10,6 +10,25 @@ import (
10 10
 // simple types
11 11
 //
12 12
 
13
+type CapSubCommand string
14
+
15
+type Capability string
16
+
17
+type CapModifier rune
18
+
19
+type CapState uint
20
+
21
+type CapabilitySet map[Capability]bool
22
+
23
+func (set CapabilitySet) String() string {
24
+	strs := make([]string, len(set))
25
+	index := 0
26
+	for capability := range set {
27
+		strs[index] = string(capability)
28
+	}
29
+	return strings.Join(strs, " ")
30
+}
31
+
13 32
 // a string with wildcards
14 33
 type Mask string
15 34
 
@@ -24,7 +43,7 @@ func (op ModeOp) String() string {
24 43
 type UserMode rune
25 44
 
26 45
 func (mode UserMode) String() string {
27
-	return fmt.Sprintf("%c", mode)
46
+	return string(mode)
28 47
 }
29 48
 
30 49
 type Phase uint
@@ -49,7 +68,7 @@ func (code NumericCode) String() string {
49 68
 type ChannelMode rune
50 69
 
51 70
 func (mode ChannelMode) String() string {
52
-	return fmt.Sprintf("%c", mode)
71
+	return string(mode)
53 72
 }
54 73
 
55 74
 type ChannelNameMap map[string]*Channel

Loading…
Cancel
Save