Browse Source

Merge pull request #8 from jlatt/cap-protocol

basic capability negotiation
tags/v0.1.0
Jeremy Latt 10 years ago
parent
commit
4bcd42ff34
7 changed files with 222 additions and 101 deletions
  1. 16
    8
      irc/channel.go
  2. 40
    34
      irc/client.go
  3. 20
    6
      irc/commands.go
  4. 30
    3
      irc/constants.go
  5. 19
    6
      irc/reply.go
  6. 71
    42
      irc/server.go
  7. 26
    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

+ 40
- 34
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
 //
@@ -232,7 +235,10 @@ func (client *Client) ChangeNickname(nickname string) {
232 235
 	}
233 236
 }
234 237
 
235
-func (client *Client) Reply(reply string) {
238
+func (client *Client) Reply(reply string, args ...interface{}) {
239
+	if len(args) > 0 {
240
+		reply = fmt.Sprintf(reply, args...)
241
+	}
236 242
 	client.socket.Write(reply)
237 243
 }
238 244
 

+ 20
- 6
irc/commands.go View File

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

+ 71
- 42
irc/server.go View File

@@ -29,7 +29,6 @@ type Server struct {
29 29
 	operators map[string][]byte
30 30
 	password  []byte
31 31
 	signals   chan os.Signal
32
-	timeout   chan *Client
33 32
 }
34 33
 
35 34
 func NewServer(config *Config) *Server {
@@ -45,7 +44,6 @@ func NewServer(config *Config) *Server {
45 44
 		newConns:  make(chan net.Conn, 16),
46 45
 		operators: config.Operators(),
47 46
 		signals:   make(chan os.Signal, 1),
48
-		timeout:   make(chan *Client, 16),
49 47
 	}
50 48
 
51 49
 	if config.Server.Password != "" {
@@ -97,14 +95,6 @@ func (server *Server) processCommand(cmd Command) {
97 95
 	}
98 96
 
99 97
 	switch client.phase {
100
-	case Authorization:
101
-		authCmd, ok := cmd.(AuthServerCommand)
102
-		if !ok {
103
-			client.Quit("unexpected command")
104
-			return
105
-		}
106
-		authCmd.HandleAuthServer(server)
107
-
108 98
 	case Registration:
109 99
 		regCmd, ok := cmd.(RegServerCommand)
110 100
 		if !ok {
@@ -113,7 +103,7 @@ func (server *Server) processCommand(cmd Command) {
113 103
 		}
114 104
 		regCmd.HandleRegServer(server)
115 105
 
116
-	default:
106
+	case Normal:
117 107
 		srvCmd, ok := cmd.(ServerCommand)
118 108
 		if !ok {
119 109
 			client.ErrUnknownCommand(cmd.Code())
@@ -157,20 +147,10 @@ func (server *Server) Run() {
157 147
 
158 148
 		case client := <-server.idle:
159 149
 			client.Idle()
160
-
161
-		case client := <-server.timeout:
162
-			client.Quit("connection timeout")
163 150
 		}
164 151
 	}
165 152
 }
166 153
 
167
-func (server *Server) InitPhase() Phase {
168
-	if server.password == nil {
169
-		return Registration
170
-	}
171
-	return Authorization
172
-}
173
-
174 154
 //
175 155
 // listen goroutine
176 156
 //
@@ -206,7 +186,7 @@ func (s *Server) listen(addr string) {
206 186
 //
207 187
 
208 188
 func (s *Server) tryRegister(c *Client) {
209
-	if c.HasNick() && c.HasUsername() {
189
+	if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) {
210 190
 		c.Register()
211 191
 		c.RplWelcome()
212 192
 		c.RplYourHost()
@@ -266,18 +246,10 @@ func (s *Server) Nick() string {
266 246
 }
267 247
 
268 248
 //
269
-// authorization commands
249
+// registration commands
270 250
 //
271 251
 
272
-func (msg *ProxyCommand) HandleAuthServer(server *Server) {
273
-	msg.Client().hostname = msg.hostname
274
-}
275
-
276
-func (msg *CapCommand) HandleAuthServer(server *Server) {
277
-	// TODO
278
-}
279
-
280
-func (msg *PassCommand) HandleAuthServer(server *Server) {
252
+func (msg *PassCommand) HandleRegServer(server *Server) {
281 253
 	client := msg.Client()
282 254
 	if msg.err != nil {
283 255
 		client.ErrPasswdMismatch()
@@ -285,27 +257,70 @@ func (msg *PassCommand) HandleAuthServer(server *Server) {
285 257
 		return
286 258
 	}
287 259
 
288
-	client.phase = Registration
260
+	client.authorized = true
289 261
 }
290 262
 
291
-func (msg *QuitCommand) HandleAuthServer(server *Server) {
292
-	msg.Client().Quit(msg.message)
293
-}
294
-
295
-//
296
-// registration commands
297
-//
298
-
299 263
 func (msg *ProxyCommand) HandleRegServer(server *Server) {
300 264
 	msg.Client().hostname = msg.hostname
301 265
 }
302 266
 
303 267
 func (msg *CapCommand) HandleRegServer(server *Server) {
304
-	// TODO
268
+	client := msg.Client()
269
+
270
+	switch msg.subCommand {
271
+	case CAP_LS:
272
+		client.capState = CapNegotiating
273
+		client.Reply("CAP LS * :%s", SupportedCapabilities)
274
+
275
+	case CAP_LIST:
276
+		client.Reply("CAP LIST * :%s", client.capabilities)
277
+
278
+	case CAP_REQ:
279
+		client.capState = CapNegotiating
280
+		for capability := range msg.capabilities {
281
+			if !SupportedCapabilities[capability] {
282
+				client.Reply("CAP NAK * :%s", msg.capabilities)
283
+				return
284
+			}
285
+		}
286
+		for capability := range msg.capabilities {
287
+			client.capabilities[capability] = true
288
+		}
289
+		client.Reply("CAP ACK * :%s", msg.capabilities)
290
+
291
+	case CAP_CLEAR:
292
+		format := strings.TrimRight(
293
+			strings.Repeat("%s%s ", len(client.capabilities)), " ")
294
+		args := make([]interface{}, len(client.capabilities))
295
+		index := 0
296
+		for capability := range client.capabilities {
297
+			args[index] = Disable
298
+			args[index+1] = capability
299
+			index += 2
300
+			delete(client.capabilities, capability)
301
+		}
302
+		client.Reply("CAP ACK * :"+format, args...)
303
+
304
+	case CAP_END:
305
+		client.capState = CapNegotiated
306
+		server.tryRegister(client)
307
+
308
+	default:
309
+		client.ErrInvalidCapCmd(msg.subCommand)
310
+	}
305 311
 }
306 312
 
307 313
 func (m *NickCommand) HandleRegServer(s *Server) {
308 314
 	client := m.Client()
315
+	if !client.authorized {
316
+		client.ErrPasswdMismatch()
317
+		client.Quit("bad password")
318
+		return
319
+	}
320
+
321
+	if client.capState == CapNegotiating {
322
+		client.capState = CapNegotiated
323
+	}
309 324
 
310 325
 	if m.nickname == "" {
311 326
 		client.ErrNoNicknameGiven()
@@ -327,11 +342,22 @@ func (m *NickCommand) HandleRegServer(s *Server) {
327 342
 }
328 343
 
329 344
 func (msg *RFC1459UserCommand) HandleRegServer(server *Server) {
345
+	client := msg.Client()
346
+	if !client.authorized {
347
+		client.ErrPasswdMismatch()
348
+		client.Quit("bad password")
349
+		return
350
+	}
330 351
 	msg.HandleRegServer2(server)
331 352
 }
332 353
 
333 354
 func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
334 355
 	client := msg.Client()
356
+	if !client.authorized {
357
+		client.ErrPasswdMismatch()
358
+		client.Quit("bad password")
359
+		return
360
+	}
335 361
 	flags := msg.Flags()
336 362
 	if len(flags) > 0 {
337 363
 		for _, mode := range msg.Flags() {
@@ -344,6 +370,9 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
344 370
 
345 371
 func (msg *UserCommand) HandleRegServer2(server *Server) {
346 372
 	client := msg.Client()
373
+	if client.capState == CapNegotiating {
374
+		client.capState = CapNegotiated
375
+	}
347 376
 	client.username, client.realname = msg.username, msg.realname
348 377
 	server.tryRegister(client)
349 378
 }

+ 26
- 2
irc/types.go View File

@@ -10,6 +10,30 @@ 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
+func (mod CapModifier) String() string {
20
+	return string(mod)
21
+}
22
+
23
+type CapState uint
24
+
25
+type CapabilitySet map[Capability]bool
26
+
27
+func (set CapabilitySet) String() string {
28
+	strs := make([]string, len(set))
29
+	index := 0
30
+	for capability := range set {
31
+		strs[index] = string(capability)
32
+		index += 1
33
+	}
34
+	return strings.Join(strs, " ")
35
+}
36
+
13 37
 // a string with wildcards
14 38
 type Mask string
15 39
 
@@ -24,7 +48,7 @@ func (op ModeOp) String() string {
24 48
 type UserMode rune
25 49
 
26 50
 func (mode UserMode) String() string {
27
-	return fmt.Sprintf("%c", mode)
51
+	return string(mode)
28 52
 }
29 53
 
30 54
 type Phase uint
@@ -49,7 +73,7 @@ func (code NumericCode) String() string {
49 73
 type ChannelMode rune
50 74
 
51 75
 func (mode ChannelMode) String() string {
52
-	return fmt.Sprintf("%c", mode)
76
+	return string(mode)
53 77
 }
54 78
 
55 79
 type ChannelNameMap map[string]*Channel

Loading…
Cancel
Save