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
 	return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator)
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
 	nicks := make([]string, len(channel.members))
59
 	nicks := make([]string, len(channel.members))
59
 	i := 0
60
 	i := 0
60
 	for client, modes := range channel.members {
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
 		i += 1
77
 		i += 1
70
 	}
78
 	}
71
 	return nicks
79
 	return nicks

+ 40
- 34
irc/client.go View File

12
 }
12
 }
13
 
13
 
14
 type Client struct {
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
 func NewClient(server *Server, conn net.Conn) *Client {
38
 func NewClient(server *Server, conn net.Conn) *Client {
36
 	now := time.Now()
39
 	now := time.Now()
37
 	client := &Client{
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
 	client.socket = NewSocket(conn, client.commands)
51
 	client.socket = NewSocket(conn, client.commands)
47
 	client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout)
52
 	client.loginTimer = time.AfterFunc(LOGIN_TIMEOUT, client.connectionTimeout)
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
 // idle timer goroutine
83
 // idle timer goroutine
73
 //
84
 //
76
 	client.server.idle <- client
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
 // server goroutine
91
 // server goroutine
89
 //
92
 //
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
 	client.socket.Write(reply)
242
 	client.socket.Write(reply)
237
 }
243
 }
238
 
244
 

+ 20
- 6
irc/commands.go View File

705
 	return cmd, nil
705
 	return cmd, nil
706
 }
706
 }
707
 
707
 
708
-// TODO
709
 type CapCommand struct {
708
 type CapCommand struct {
710
 	BaseCommand
709
 	BaseCommand
711
-	args []string
710
+	subCommand   CapSubCommand
711
+	capabilities CapabilitySet
712
 }
712
 }
713
 
713
 
714
 func (msg *CapCommand) String() string {
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
 func NewCapCommand(args []string) (editableCommand, error) {
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
 // HAPROXY support
738
 // HAPROXY support

+ 30
- 3
irc/constants.go View File

155
 	ERR_TOOMANYTARGETS    NumericCode = 407
155
 	ERR_TOOMANYTARGETS    NumericCode = 407
156
 	ERR_NOSUCHSERVICE     NumericCode = 408
156
 	ERR_NOSUCHSERVICE     NumericCode = 408
157
 	ERR_NOORIGIN          NumericCode = 409
157
 	ERR_NOORIGIN          NumericCode = 409
158
+	ERR_INVALIDCAPCMD     NumericCode = 410
158
 	ERR_NORECIPIENT       NumericCode = 411
159
 	ERR_NORECIPIENT       NumericCode = 411
159
 	ERR_NOTEXTTOSEND      NumericCode = 412
160
 	ERR_NOTEXTTOSEND      NumericCode = 412
160
 	ERR_NOTOPLEVEL        NumericCode = 413
161
 	ERR_NOTOPLEVEL        NumericCode = 413
200
 	ERR_UMODEUNKNOWNFLAG  NumericCode = 501
201
 	ERR_UMODEUNKNOWNFLAG  NumericCode = 501
201
 	ERR_USERSDONTMATCH    NumericCode = 502
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
 	Add    ModeOp = '+'
212
 	Add    ModeOp = '+'
204
 	List   ModeOp = '='
213
 	List   ModeOp = '='
205
 	Remove ModeOp = '-'
214
 	Remove ModeOp = '-'
230
 	Secret          ChannelMode = 's' // flag, deprecated
239
 	Secret          ChannelMode = 's' // flag, deprecated
231
 	UserLimit       ChannelMode = 'l' // flag arg
240
 	UserLimit       ChannelMode = 'l' // flag arg
232
 	Voice           ChannelMode = 'v' // arg
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
 const (
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
 
240
 
241
 	if channel != nil {
241
 	if channel != nil {
242
 		channelName = channel.name
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
 	target.NumericReply(RPL_WHOREPLY,
258
 	target.NumericReply(RPL_WHOREPLY,
360
 }
368
 }
361
 
369
 
362
 func (target *Client) RplNamReply(channel *Channel) {
370
 func (target *Client) RplNamReply(channel *Channel) {
363
-	target.MultilineReply(channel.Nicks(), RPL_NAMREPLY,
371
+	target.MultilineReply(channel.Nicks(target), RPL_NAMREPLY,
364
 		"= %s :%s", channel)
372
 		"= %s :%s", channel)
365
 }
373
 }
366
 
374
 
502
 	target.NumericReply(ERR_CHANNELISFULL,
510
 	target.NumericReply(ERR_CHANNELISFULL,
503
 		"%s :Cannot join channel (+l)", channel)
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
 	operators map[string][]byte
29
 	operators map[string][]byte
30
 	password  []byte
30
 	password  []byte
31
 	signals   chan os.Signal
31
 	signals   chan os.Signal
32
-	timeout   chan *Client
33
 }
32
 }
34
 
33
 
35
 func NewServer(config *Config) *Server {
34
 func NewServer(config *Config) *Server {
45
 		newConns:  make(chan net.Conn, 16),
44
 		newConns:  make(chan net.Conn, 16),
46
 		operators: config.Operators(),
45
 		operators: config.Operators(),
47
 		signals:   make(chan os.Signal, 1),
46
 		signals:   make(chan os.Signal, 1),
48
-		timeout:   make(chan *Client, 16),
49
 	}
47
 	}
50
 
48
 
51
 	if config.Server.Password != "" {
49
 	if config.Server.Password != "" {
97
 	}
95
 	}
98
 
96
 
99
 	switch client.phase {
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
 	case Registration:
98
 	case Registration:
109
 		regCmd, ok := cmd.(RegServerCommand)
99
 		regCmd, ok := cmd.(RegServerCommand)
110
 		if !ok {
100
 		if !ok {
113
 		}
103
 		}
114
 		regCmd.HandleRegServer(server)
104
 		regCmd.HandleRegServer(server)
115
 
105
 
116
-	default:
106
+	case Normal:
117
 		srvCmd, ok := cmd.(ServerCommand)
107
 		srvCmd, ok := cmd.(ServerCommand)
118
 		if !ok {
108
 		if !ok {
119
 			client.ErrUnknownCommand(cmd.Code())
109
 			client.ErrUnknownCommand(cmd.Code())
157
 
147
 
158
 		case client := <-server.idle:
148
 		case client := <-server.idle:
159
 			client.Idle()
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
 // listen goroutine
155
 // listen goroutine
176
 //
156
 //
206
 //
186
 //
207
 
187
 
208
 func (s *Server) tryRegister(c *Client) {
188
 func (s *Server) tryRegister(c *Client) {
209
-	if c.HasNick() && c.HasUsername() {
189
+	if c.HasNick() && c.HasUsername() && (c.capState != CapNegotiating) {
210
 		c.Register()
190
 		c.Register()
211
 		c.RplWelcome()
191
 		c.RplWelcome()
212
 		c.RplYourHost()
192
 		c.RplYourHost()
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
 	client := msg.Client()
253
 	client := msg.Client()
282
 	if msg.err != nil {
254
 	if msg.err != nil {
283
 		client.ErrPasswdMismatch()
255
 		client.ErrPasswdMismatch()
285
 		return
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
 func (msg *ProxyCommand) HandleRegServer(server *Server) {
263
 func (msg *ProxyCommand) HandleRegServer(server *Server) {
300
 	msg.Client().hostname = msg.hostname
264
 	msg.Client().hostname = msg.hostname
301
 }
265
 }
302
 
266
 
303
 func (msg *CapCommand) HandleRegServer(server *Server) {
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
 func (m *NickCommand) HandleRegServer(s *Server) {
313
 func (m *NickCommand) HandleRegServer(s *Server) {
308
 	client := m.Client()
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
 	if m.nickname == "" {
325
 	if m.nickname == "" {
311
 		client.ErrNoNicknameGiven()
326
 		client.ErrNoNicknameGiven()
327
 }
342
 }
328
 
343
 
329
 func (msg *RFC1459UserCommand) HandleRegServer(server *Server) {
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
 	msg.HandleRegServer2(server)
351
 	msg.HandleRegServer2(server)
331
 }
352
 }
332
 
353
 
333
 func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
354
 func (msg *RFC2812UserCommand) HandleRegServer(server *Server) {
334
 	client := msg.Client()
355
 	client := msg.Client()
356
+	if !client.authorized {
357
+		client.ErrPasswdMismatch()
358
+		client.Quit("bad password")
359
+		return
360
+	}
335
 	flags := msg.Flags()
361
 	flags := msg.Flags()
336
 	if len(flags) > 0 {
362
 	if len(flags) > 0 {
337
 		for _, mode := range msg.Flags() {
363
 		for _, mode := range msg.Flags() {
344
 
370
 
345
 func (msg *UserCommand) HandleRegServer2(server *Server) {
371
 func (msg *UserCommand) HandleRegServer2(server *Server) {
346
 	client := msg.Client()
372
 	client := msg.Client()
373
+	if client.capState == CapNegotiating {
374
+		client.capState = CapNegotiated
375
+	}
347
 	client.username, client.realname = msg.username, msg.realname
376
 	client.username, client.realname = msg.username, msg.realname
348
 	server.tryRegister(client)
377
 	server.tryRegister(client)
349
 }
378
 }

+ 26
- 2
irc/types.go View File

10
 // simple types
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
 // a string with wildcards
37
 // a string with wildcards
14
 type Mask string
38
 type Mask string
15
 
39
 
24
 type UserMode rune
48
 type UserMode rune
25
 
49
 
26
 func (mode UserMode) String() string {
50
 func (mode UserMode) String() string {
27
-	return fmt.Sprintf("%c", mode)
51
+	return string(mode)
28
 }
52
 }
29
 
53
 
30
 type Phase uint
54
 type Phase uint
49
 type ChannelMode rune
73
 type ChannelMode rune
50
 
74
 
51
 func (mode ChannelMode) String() string {
75
 func (mode ChannelMode) String() string {
52
-	return fmt.Sprintf("%c", mode)
76
+	return string(mode)
53
 }
77
 }
54
 
78
 
55
 type ChannelNameMap map[string]*Channel
79
 type ChannelNameMap map[string]*Channel

Loading…
Cancel
Save