Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(96)

Delta Between Two Patch Sets: ssh/server.go

Issue 14225043: code review 14225043: go.crypto/ssh: reimplement SSH connection protocol modu... (Closed)
Left Patch Set: diff -r 5ff5636e18c9 https://code.google.com/p/go.crypto Created 10 years, 5 months ago
Right Patch Set: diff -r cd1eea1eb828 https://code.google.com/p/go.crypto Created 10 years, 5 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « ssh/mux_test.go ('k') | ssh/server_terminal.go » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 // Copyright 2011 The Go Authors. All rights reserved. 1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style 2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file. 3 // license that can be found in the LICENSE file.
4 4
5 package ssh 5 package ssh
6 6
7 import ( 7 import (
8 "bytes" 8 "bytes"
9 "crypto/rand" 9 "crypto/rand"
10 "errors" 10 "errors"
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after
88 type cachedPubKey struct { 88 type cachedPubKey struct {
89 user, algo string 89 user, algo string
90 pubKey []byte 90 pubKey []byte
91 result bool 91 result bool
92 } 92 }
93 93
94 const maxCachedPubKeys = 16 94 const maxCachedPubKeys = 16
95 95
96 // A ServerConn represents an incoming connection. 96 // A ServerConn represents an incoming connection.
97 type ServerConn struct { 97 type ServerConn struct {
98 » *transport 98 » transport *transport
99 » config *ServerConfig 99 » config *ServerConfig
100 100
101 // cachedPubKeys contains the cache results of tests for public keys. 101 // cachedPubKeys contains the cache results of tests for public keys.
102 // Since SSH clients will query whether a public key is acceptable 102 // Since SSH clients will query whether a public key is acceptable
103 // before attempting to authenticate with it, we end up with duplicate 103 // before attempting to authenticate with it, we end up with duplicate
104 // queries for public key validity. 104 // queries for public key validity.
105 cachedPubKeys []cachedPubKey 105 cachedPubKeys []cachedPubKey
106 106
107 // User holds the successfully authenticated user name. 107 // User holds the successfully authenticated user name.
108 // It is empty if no authentication is used. It is populated before 108 // It is empty if no authentication is used. It is populated before
109 // any authentication callback is called and not assigned to after that. 109 // any authentication callback is called and not assigned to after that.
(...skipping 12 matching lines...) Expand all
122 mux *mux 122 mux *mux
123 } 123 }
124 124
125 // Server returns a new SSH server connection 125 // Server returns a new SSH server connection
126 // using c as the underlying transport. 126 // using c as the underlying transport.
127 func Server(c net.Conn, config *ServerConfig) *ServerConn { 127 func Server(c net.Conn, config *ServerConfig) *ServerConn {
128 tr := newTransport(c, config.rand(), false /* not client */) 128 tr := newTransport(c, config.rand(), false /* not client */)
129 return &ServerConn{ 129 return &ServerConn{
130 transport: tr, 130 transport: tr,
131 config: config, 131 config: config,
132 mux: newMux(tr),
133 } 132 }
134 } 133 }
135 134
136 // signAndMarshal signs the data with the appropriate algorithm, 135 // signAndMarshal signs the data with the appropriate algorithm,
137 // and serializes the result in SSH wire format. 136 // and serializes the result in SSH wire format.
138 func signAndMarshal(k Signer, rand io.Reader, data []byte) ([]byte, error) { 137 func signAndMarshal(k Signer, rand io.Reader, data []byte) ([]byte, error) {
139 sig, err := k.Sign(rand, data) 138 sig, err := k.Sign(rand, data)
140 if err != nil { 139 if err != nil {
141 return nil, err 140 return nil, err
142 } 141 }
143 142
144 return serializeSignature(k.PublicKey().PrivateKeyAlgo(), sig), nil 143 return serializeSignature(k.PublicKey().PrivateKeyAlgo(), sig), nil
145 } 144 }
145
146 // Close closes the connection.
147 func (s *ServerConn) Close() error { return s.transport.Close() }
148
149 // LocalAddr returns the local network address.
150 func (c *ServerConn) LocalAddr() net.Addr { return c.transport.LocalAddr() }
151
152 // RemoteAddr returns the remote network address.
153 func (c *ServerConn) RemoteAddr() net.Addr { return c.transport.RemoteAddr() }
146 154
147 // Handshake performs an SSH transport and client authentication on the given Se rverConn. 155 // Handshake performs an SSH transport and client authentication on the given Se rverConn.
148 func (s *ServerConn) Handshake() error { 156 func (s *ServerConn) Handshake() error {
149 var err error 157 var err error
150 s.serverVersion = []byte(packageVersion) 158 s.serverVersion = []byte(packageVersion)
151 s.ClientVersion, err = exchangeVersions(s.transport.Conn, s.serverVersio n) 159 s.ClientVersion, err = exchangeVersions(s.transport.Conn, s.serverVersio n)
152 if err != nil { 160 if err != nil {
153 return err 161 return err
154 } 162 }
155 if err := s.clientInitHandshake(nil, nil); err != nil { 163 if err := s.clientInitHandshake(nil, nil); err != nil {
156 return err 164 return err
157 } 165 }
158 166
159 var packet []byte 167 var packet []byte
160 » if packet, err = s.readPacket(); err != nil { 168 » if packet, err = s.transport.readPacket(); err != nil {
161 return err 169 return err
162 } 170 }
163 var serviceRequest serviceRequestMsg 171 var serviceRequest serviceRequestMsg
164 if err := unmarshal(&serviceRequest, packet, msgServiceRequest); err != nil { 172 if err := unmarshal(&serviceRequest, packet, msgServiceRequest); err != nil {
165 return err 173 return err
166 } 174 }
167 if serviceRequest.Service != serviceUserAuth { 175 if serviceRequest.Service != serviceUserAuth {
168 return errors.New("ssh: requested service '" + serviceRequest.Se rvice + "' before authenticating") 176 return errors.New("ssh: requested service '" + serviceRequest.Se rvice + "' before authenticating")
169 } 177 }
170 serviceAccept := serviceAcceptMsg{ 178 serviceAccept := serviceAcceptMsg{
171 Service: serviceUserAuth, 179 Service: serviceUserAuth,
172 } 180 }
173 » if err := s.writePacket(marshal(msgServiceAccept, serviceAccept)); err ! = nil { 181 » if err := s.transport.writePacket(marshal(msgServiceAccept, serviceAccep t)); err != nil {
174 » » return err 182 » » return err
175 » } 183 » }
176 184
177 » if err := s.authenticate(s.transport.sessionID); err != nil { 185 » if err := s.authenticate(); err != nil {
178 » » return err 186 » » return err
179 » } 187 » }
180 188
189 » s.mux = newMux(s.transport)
190 » go s.handleGlobalRequests()
181 go s.mux.Loop() 191 go s.mux.Loop()
182 go s.handleGlobalRequests()
183 return err 192 return err
184 } 193 }
185 194
186 func (s *ServerConn) handleGlobalRequests() { 195 func (s *ServerConn) handleGlobalRequests() {
187 » for r := range s.mux.IncomingRequests() { 196 » for r := range s.mux.incomingRequests {
188 if r.WantReply { 197 if r.WantReply {
189 s.mux.AckRequest(false, nil) 198 s.mux.AckRequest(false, nil)
190 } 199 }
191 } 200 }
192 } 201 }
193 202
194 func (s *ServerConn) clientInitHandshake(clientKexInit *kexInitMsg, clientKexIni tPacket []byte) (err error) { 203 func (s *ServerConn) clientInitHandshake(clientKexInit *kexInitMsg, clientKexIni tPacket []byte) (err error) {
195 serverKexInit := kexInitMsg{ 204 serverKexInit := kexInitMsg{
196 KexAlgos: s.config.Crypto.kexes(), 205 KexAlgos: s.config.Crypto.kexes(),
197 CiphersClientServer: s.config.Crypto.ciphers(), 206 CiphersClientServer: s.config.Crypto.ciphers(),
198 CiphersServerClient: s.config.Crypto.ciphers(), 207 CiphersServerClient: s.config.Crypto.ciphers(),
199 MACsClientServer: s.config.Crypto.macs(), 208 MACsClientServer: s.config.Crypto.macs(),
200 MACsServerClient: s.config.Crypto.macs(), 209 MACsServerClient: s.config.Crypto.macs(),
201 CompressionClientServer: supportedCompressions, 210 CompressionClientServer: supportedCompressions,
202 CompressionServerClient: supportedCompressions, 211 CompressionServerClient: supportedCompressions,
203 } 212 }
204 for _, k := range s.config.hostKeys { 213 for _, k := range s.config.hostKeys {
205 serverKexInit.ServerHostKeyAlgos = append( 214 serverKexInit.ServerHostKeyAlgos = append(
206 serverKexInit.ServerHostKeyAlgos, k.PublicKey().PublicKe yAlgo()) 215 serverKexInit.ServerHostKeyAlgos, k.PublicKey().PublicKe yAlgo())
207 } 216 }
208 217
209 serverKexInitPacket := marshal(msgKexInit, serverKexInit) 218 serverKexInitPacket := marshal(msgKexInit, serverKexInit)
210 » if err = s.writePacket(serverKexInitPacket); err != nil { 219 » if err = s.transport.writePacket(serverKexInitPacket); err != nil {
211 return 220 return
212 } 221 }
213 222
214 if clientKexInitPacket == nil { 223 if clientKexInitPacket == nil {
215 clientKexInit = new(kexInitMsg) 224 clientKexInit = new(kexInitMsg)
216 » » if clientKexInitPacket, err = s.readPacket(); err != nil { 225 » » if clientKexInitPacket, err = s.transport.readPacket(); err != n il {
217 return 226 return
218 } 227 }
219 if err = unmarshal(clientKexInit, clientKexInitPacket, msgKexIni t); err != nil { 228 if err = unmarshal(clientKexInit, clientKexInitPacket, msgKexIni t); err != nil {
220 return 229 return
221 } 230 }
222 } 231 }
223 232
224 algs := findAgreedAlgorithms(clientKexInit, &serverKexInit) 233 algs := findAgreedAlgorithms(clientKexInit, &serverKexInit)
225 if algs == nil { 234 if algs == nil {
226 return errors.New("ssh: no common algorithms") 235 return errors.New("ssh: no common algorithms")
227 } 236 }
228 237
229 if clientKexInit.FirstKexFollows && algs.kex != clientKexInit.KexAlgos[0 ] { 238 if clientKexInit.FirstKexFollows && algs.kex != clientKexInit.KexAlgos[0 ] {
230 // The client sent a Kex message for the wrong algorithm, 239 // The client sent a Kex message for the wrong algorithm,
231 // which we have to ignore. 240 // which we have to ignore.
232 » » if _, err = s.readPacket(); err != nil { 241 » » if _, err = s.transport.readPacket(); err != nil {
233 return 242 return
234 } 243 }
235 } 244 }
236 245
237 var hostKey Signer 246 var hostKey Signer
238 for _, k := range s.config.hostKeys { 247 for _, k := range s.config.hostKeys {
239 if algs.hostKey == k.PublicKey().PublicKeyAlgo() { 248 if algs.hostKey == k.PublicKey().PublicKeyAlgo() {
240 hostKey = k 249 hostKey = k
241 } 250 }
242 } 251 }
243 252
244 kex, ok := kexAlgoMap[algs.kex] 253 kex, ok := kexAlgoMap[algs.kex]
245 if !ok { 254 if !ok {
246 return fmt.Errorf("ssh: unexpected key exchange algorithm %v", a lgs.kex) 255 return fmt.Errorf("ssh: unexpected key exchange algorithm %v", a lgs.kex)
247 } 256 }
248 257
249 magics := handshakeMagics{ 258 magics := handshakeMagics{
250 serverVersion: s.serverVersion, 259 serverVersion: s.serverVersion,
251 clientVersion: s.ClientVersion, 260 clientVersion: s.ClientVersion,
252 serverKexInit: marshal(msgKexInit, serverKexInit), 261 serverKexInit: marshal(msgKexInit, serverKexInit),
253 clientKexInit: clientKexInitPacket, 262 clientKexInit: clientKexInitPacket,
254 } 263 }
255 » result, err := kex.Server(s, s.config.rand(), &magics, hostKey) 264 » result, err := kex.Server(s.transport, s.config.rand(), &magics, hostKey )
256 if err != nil { 265 if err != nil {
257 return err 266 return err
258 } 267 }
259 268
260 if err = s.transport.prepareKeyChange(algs, result); err != nil { 269 if err = s.transport.prepareKeyChange(algs, result); err != nil {
261 return err 270 return err
262 } 271 }
263 272
264 » if err = s.writePacket([]byte{msgNewKeys}); err != nil { 273 » if err = s.transport.writePacket([]byte{msgNewKeys}); err != nil {
265 return 274 return
266 } 275 }
267 » if packet, err := s.readPacket(); err != nil { 276 » if packet, err := s.transport.readPacket(); err != nil {
268 return err 277 return err
269 } else if packet[0] != msgNewKeys { 278 } else if packet[0] != msgNewKeys {
270 return UnexpectedMessageError{msgNewKeys, packet[0]} 279 return UnexpectedMessageError{msgNewKeys, packet[0]}
271 } 280 }
272 281
273 return 282 return
274 } 283 }
275 284
276 func isAcceptableAlgo(algo string) bool { 285 func isAcceptableAlgo(algo string) bool {
277 switch algo { 286 switch algo {
(...skipping 24 matching lines...) Expand all
302 pubKey: make([]byte, len(pubKey)), 311 pubKey: make([]byte, len(pubKey)),
303 result: result, 312 result: result,
304 } 313 }
305 copy(c.pubKey, pubKey) 314 copy(c.pubKey, pubKey)
306 s.cachedPubKeys = append(s.cachedPubKeys, c) 315 s.cachedPubKeys = append(s.cachedPubKeys, c)
307 } 316 }
308 317
309 return result 318 return result
310 } 319 }
311 320
312 func (s *ServerConn) authenticate(H []byte) error { 321 func (s *ServerConn) authenticate() error {
313 var userAuthReq userAuthRequestMsg 322 var userAuthReq userAuthRequestMsg
314 var err error 323 var err error
315 var packet []byte 324 var packet []byte
316 325
317 userAuthLoop: 326 userAuthLoop:
318 for { 327 for {
319 » » if packet, err = s.readPacket(); err != nil { 328 » » if packet, err = s.transport.readPacket(); err != nil {
320 return err 329 return err
321 } 330 }
322 if err = unmarshal(&userAuthReq, packet, msgUserAuthRequest); er r != nil { 331 if err = unmarshal(&userAuthReq, packet, msgUserAuthRequest); er r != nil {
323 return err 332 return err
324 } 333 }
325 334
326 if userAuthReq.Service != serviceSSH { 335 if userAuthReq.Service != serviceSSH {
327 return errors.New("ssh: client attempted to negotiate fo r unknown service: " + userAuthReq.Service) 336 return errors.New("ssh: client attempted to negotiate fo r unknown service: " + userAuthReq.Service)
328 } 337 }
329 338
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
383 // The client can query if the given public key 392 // The client can query if the given public key
384 // would be okay. 393 // would be okay.
385 if len(payload) > 0 { 394 if len(payload) > 0 {
386 return ParseError{msgUserAuthRequest} 395 return ParseError{msgUserAuthRequest}
387 } 396 }
388 if s.testPubKey(userAuthReq.User, algo, pubKey) { 397 if s.testPubKey(userAuthReq.User, algo, pubKey) {
389 okMsg := userAuthPubKeyOkMsg{ 398 okMsg := userAuthPubKeyOkMsg{
390 Algo: algo, 399 Algo: algo,
391 PubKey: string(pubKey), 400 PubKey: string(pubKey),
392 } 401 }
393 » » » » » if err = s.writePacket(marshal(msgUserAu thPubKeyOk, okMsg)); err != nil { 402 » » » » » if err = s.transport.writePacket(marshal (msgUserAuthPubKeyOk, okMsg)); err != nil {
394 return err 403 return err
395 } 404 }
396 continue userAuthLoop 405 continue userAuthLoop
397 } 406 }
398 } else { 407 } else {
399 sig, payload, ok := parseSignature(payload) 408 sig, payload, ok := parseSignature(payload)
400 if !ok || len(payload) > 0 { 409 if !ok || len(payload) > 0 {
401 return ParseError{msgUserAuthRequest} 410 return ParseError{msgUserAuthRequest}
402 } 411 }
403 // Ensure the public key algo and signature algo 412 // Ensure the public key algo and signature algo
404 // are supported. Compare the private key 413 // are supported. Compare the private key
405 // algorithm name that corresponds to algo with 414 // algorithm name that corresponds to algo with
406 // sig.Format. This is usually the same, but 415 // sig.Format. This is usually the same, but
407 // for certs, the names differ. 416 // for certs, the names differ.
408 if !isAcceptableAlgo(algo) || !isAcceptableAlgo( sig.Format) || pubAlgoToPrivAlgo(algo) != sig.Format { 417 if !isAcceptableAlgo(algo) || !isAcceptableAlgo( sig.Format) || pubAlgoToPrivAlgo(algo) != sig.Format {
409 break 418 break
410 } 419 }
411 » » » » signedData := buildDataSignedForAuth(H, userAuth Req, algoBytes, pubKey) 420 » » » » signedData := buildDataSignedForAuth(s.transport .sessionID, userAuthReq, algoBytes, pubKey)
412 key, _, ok := ParsePublicKey(pubKey) 421 key, _, ok := ParsePublicKey(pubKey)
413 if !ok { 422 if !ok {
414 return ParseError{msgUserAuthRequest} 423 return ParseError{msgUserAuthRequest}
415 } 424 }
416 425
417 if !key.Verify(signedData, sig.Blob) { 426 if !key.Verify(signedData, sig.Blob) {
418 return ParseError{msgUserAuthRequest} 427 return ParseError{msgUserAuthRequest}
419 } 428 }
420 // TODO(jmpittman): Implement full validation fo r certificates. 429 // TODO(jmpittman): Implement full validation fo r certificates.
421 s.User = userAuthReq.User 430 s.User = userAuthReq.User
(...skipping 11 matching lines...) Expand all
433 failureMsg.Methods = append(failureMsg.Methods, "publick ey") 442 failureMsg.Methods = append(failureMsg.Methods, "publick ey")
434 } 443 }
435 if s.config.KeyboardInteractiveCallback != nil { 444 if s.config.KeyboardInteractiveCallback != nil {
436 failureMsg.Methods = append(failureMsg.Methods, "keyboar d-interactive") 445 failureMsg.Methods = append(failureMsg.Methods, "keyboar d-interactive")
437 } 446 }
438 447
439 if len(failureMsg.Methods) == 0 { 448 if len(failureMsg.Methods) == 0 {
440 return errors.New("ssh: no authentication methods config ured but NoClientAuth is also false") 449 return errors.New("ssh: no authentication methods config ured but NoClientAuth is also false")
441 } 450 }
442 451
443 » » if err = s.writePacket(marshal(msgUserAuthFailure, failureMsg)); err != nil { 452 » » if err = s.transport.writePacket(marshal(msgUserAuthFailure, fai lureMsg)); err != nil {
444 return err 453 return err
445 } 454 }
446 } 455 }
447 456
448 packet = []byte{msgUserAuthSuccess} 457 packet = []byte{msgUserAuthSuccess}
449 » if err = s.writePacket(packet); err != nil { 458 » if err = s.transport.writePacket(packet); err != nil {
450 return err 459 return err
451 } 460 }
452 461
453 return nil 462 return nil
454 } 463 }
455 464
456 // sshClientKeyboardInteractive implements a ClientKeyboardInteractive by 465 // sshClientKeyboardInteractive implements a ClientKeyboardInteractive by
457 // asking the client on the other side of a ServerConn. 466 // asking the client on the other side of a ServerConn.
458 type sshClientKeyboardInteractive struct { 467 type sshClientKeyboardInteractive struct {
459 *ServerConn 468 *ServerConn
460 } 469 }
461 470
462 func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, quest ions []string, echos []bool) (answers []string, err error) { 471 func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, quest ions []string, echos []bool) (answers []string, err error) {
463 if len(questions) != len(echos) { 472 if len(questions) != len(echos) {
464 return nil, errors.New("ssh: echos and questions must have equal length") 473 return nil, errors.New("ssh: echos and questions must have equal length")
465 } 474 }
466 475
467 var prompts []byte 476 var prompts []byte
468 for i := range questions { 477 for i := range questions {
469 prompts = appendString(prompts, questions[i]) 478 prompts = appendString(prompts, questions[i])
470 prompts = appendBool(prompts, echos[i]) 479 prompts = appendBool(prompts, echos[i])
471 } 480 }
472 481
473 » if err := c.writePacket(marshal(msgUserAuthInfoRequest, userAuthInfoRequ estMsg{ 482 » if err := c.transport.writePacket(marshal(msgUserAuthInfoRequest, userAu thInfoRequestMsg{
474 Instruction: instruction, 483 Instruction: instruction,
475 NumPrompts: uint32(len(questions)), 484 NumPrompts: uint32(len(questions)),
476 Prompts: prompts, 485 Prompts: prompts,
477 })); err != nil { 486 })); err != nil {
478 return nil, err 487 return nil, err
479 } 488 }
480 489
481 » packet, err := c.readPacket() 490 » packet, err := c.transport.readPacket()
482 if err != nil { 491 if err != nil {
483 return nil, err 492 return nil, err
484 } 493 }
485 if packet[0] != msgUserAuthInfoResponse { 494 if packet[0] != msgUserAuthInfoResponse {
486 return nil, UnexpectedMessageError{msgUserAuthInfoResponse, pack et[0]} 495 return nil, UnexpectedMessageError{msgUserAuthInfoResponse, pack et[0]}
487 } 496 }
488 packet = packet[1:] 497 packet = packet[1:]
489 498
490 n, packet, ok := parseUint32(packet) 499 n, packet, ok := parseUint32(packet)
491 if !ok || int(n) != len(questions) { 500 if !ok || int(n) != len(questions) {
(...skipping 13 matching lines...) Expand all
505 return nil, errors.New("ssh: junk at end of message") 514 return nil, errors.New("ssh: junk at end of message")
506 } 515 }
507 516
508 return answers, nil 517 return answers, nil
509 } 518 }
510 519
511 const defaultWindowSize = 32768 520 const defaultWindowSize = 32768
512 521
513 // Accept reads and processes messages on a ServerConn. It must be called 522 // Accept reads and processes messages on a ServerConn. It must be called
514 // in order to demultiplex messages to any resulting Channels. 523 // in order to demultiplex messages to any resulting Channels.
515 func (s *ServerConn) Accept() (ChannelCreationRequest, error) { 524 func (s *ServerConn) Accept() (Channel, error) {
516 » ch, ok := <-s.mux.IncomingChannels() 525 » in, ok := <-s.mux.incomingChannels
517 if !ok { 526 if !ok {
518 return nil, io.EOF 527 return nil, io.EOF
519 } 528 }
520 » return ch, nil 529 » return newCompatChannel(in), nil
521 } 530 }
522 531
523 // A Listener implements a network listener (net.Listener) for SSH connections. 532 // A Listener implements a network listener (net.Listener) for SSH connections.
524 type Listener struct { 533 type Listener struct {
525 listener net.Listener 534 listener net.Listener
526 config *ServerConfig 535 config *ServerConfig
527 } 536 }
528 537
529 // Addr returns the listener's network address. 538 // Addr returns the listener's network address.
530 func (l *Listener) Addr() net.Addr { 539 func (l *Listener) Addr() net.Addr {
(...skipping 21 matching lines...) Expand all
552 func Listen(network, addr string, config *ServerConfig) (*Listener, error) { 561 func Listen(network, addr string, config *ServerConfig) (*Listener, error) {
553 l, err := net.Listen(network, addr) 562 l, err := net.Listen(network, addr)
554 if err != nil { 563 if err != nil {
555 return nil, err 564 return nil, err
556 } 565 }
557 return &Listener{ 566 return &Listener{
558 l, 567 l,
559 config, 568 config,
560 }, nil 569 }, nil
561 } 570 }
LEFTRIGHT

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b