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

Delta Between Two Patch Sets: ssh/channel.go

Issue 14225043: code review 14225043: go.crypto/ssh: reimplement SSH connection protocol modu... (Closed)
Left Patch Set: diff -r 2cd6b3b93cdb 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 | « no previous file | ssh/client.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 "encoding/binary" 8 "encoding/binary"
9 "errors" 9 "errors"
10 "fmt" 10 "fmt"
11 "io" 11 "io"
12 "log" 12 "log"
13 "sync" 13 "sync"
14 ) 14 )
15 15
16 // A Channel is an ordered, reliable, duplex stream that is 16 // A Channel is an ordered, reliable, duplex stream that is
17 // multiplexed over an SSH connection. 17 // multiplexed over an SSH connection.
18 type Channel interface { 18 type Channel interface {
19 // Accept accepts the channel creation request. 19 // Accept accepts the channel creation request.
20 Accept() error 20 Accept() error
21 21
22 // Reject rejects the channel creation request. After calling 22 // Reject rejects the channel creation request. After calling
23 // this, no other methods on the Channel may be called. 23 // this, no other methods on the Channel may be called.
24 Reject(reason RejectionReason, message string) error 24 Reject(reason RejectionReason, message string) error
25 25
26 » // NOSUBMIT - should add ReadExtended/WriteExtended here? 26 » // Read may return a ChannelRequest as an error.
27
28 Read(data []byte) (int, error) 27 Read(data []byte) (int, error)
29 Write(data []byte) (int, error) 28 Write(data []byte) (int, error)
30
31 // Sends EOF to the other side.
32 CloseWrite() error
33 29
34 // Signals end of channel use. No data may be sent after this 30 // Signals end of channel use. No data may be sent after this
35 // call. 31 // call.
36 Close() error 32 Close() error
37 33
38 » // Stderr returns an io.ReadWriter that writes to this channel with the 34 » // Stderr returns an io.Writer that writes to this channel with the
39 // extended data type set to stderr. 35 // extended data type set to stderr.
40 » Stderr() io.ReadWriter 36 » Stderr() io.Writer
41
42 » // SendRequest sends a channel request.
43 » SendRequest(name string, wantReply bool, payload []byte) (bool, error)
44
45 » // ReceivedRequests returns the channel for out-of-band
46 » // requests. For all requests received that have WantReply, a
47 » // call of AckRequest should follow.
48 » ReceivedRequests() chan *ChannelRequest
49 37
50 // AckRequest either sends an ack or nack to the channel 38 // AckRequest either sends an ack or nack to the channel
51 // request. It should only be called if the last 39 // request. It should only be called if the last
52 // ChannelRequest had a WantReply 40 // ChannelRequest had a WantReply
53 AckRequest(ok bool) error 41 AckRequest(ok bool) error
54 42
55 // ChannelType returns the type of the channel, as supplied by the 43 // ChannelType returns the type of the channel, as supplied by the
56 // client. 44 // client.
57 ChannelType() string 45 ChannelType() string
58 46
59 » // ExtraData returns the arbitary payload for this channel, as supplied 47 » // ExtraData returns the arbitrary payload for this channel, as supplied
60 // by the client. This data is specific to the channel type. 48 // by the client. This data is specific to the channel type.
61 ExtraData() []byte 49 ExtraData() []byte
62 } 50 }
63 51
64 // ChannelRequest represents a request sent, outside of the normal 52 // ChannelRequest represents a request sent, outside of the normal
65 // stream of bytes. Requests may either be channel specific, or global 53 // stream of bytes.
66 type ChannelRequest struct { 54 type ChannelRequest struct {
67 Request string 55 Request string
68 WantReply bool 56 WantReply bool
69 Payload []byte 57 Payload []byte
58 }
59
60 func (c ChannelRequest) Error() string {
61 return "ssh: channel request received"
70 } 62 }
71 63
72 // RejectionReason is an enumeration used when rejecting channel creation 64 // RejectionReason is an enumeration used when rejecting channel creation
73 // requests. See RFC 4254, section 5.1. 65 // requests. See RFC 4254, section 5.1.
74 type RejectionReason uint32 66 type RejectionReason uint32
75 67
76 const ( 68 const (
77 Prohibited RejectionReason = iota + 1 69 Prohibited RejectionReason = iota + 1
78 ConnectionFailed 70 ConnectionFailed
79 UnknownChannelType 71 UnknownChannelType
80 ResourceShortage 72 ResourceShortage
81 ) 73 )
82 74
75 // String converts the rejection reason to human readable form.
83 func (r RejectionReason) String() string { 76 func (r RejectionReason) String() string {
84 switch r { 77 switch r {
85 case Prohibited: 78 case Prohibited:
86 » » return "Prohibited" 79 » » return "administratively prohibited"
87 case ConnectionFailed: 80 case ConnectionFailed:
88 » » return "ConnectionFailed" 81 » » return "connect failed"
89 case UnknownChannelType: 82 case UnknownChannelType:
90 » » return "UnknownChannelType" 83 » » return "unknown channel type"
91 case ResourceShortage: 84 case ResourceShortage:
92 » » return "ResourceShortage" 85 » » return "resource shortage"
93 } 86 }
94 return fmt.Sprintf("unknown reason %d", int(r)) 87 return fmt.Sprintf("unknown reason %d", int(r))
95 } 88 }
96 89
97 // channel implements the Channel 90 // channel implements the Channel
98 type channel struct { 91 type channel struct {
99 // R/O after creation 92 // R/O after creation
100 chanType string 93 chanType string
101 extraData []byte 94 extraData []byte
102 localId, remoteId uint32 95 localId, remoteId uint32
103 maxPacket uint32 96 maxPacket uint32
104 mux *mux 97 mux *mux
105 98
106 // If set, we have called Accept or Reject on this channel 99 // If set, we have called Accept or Reject on this channel
107 decided bool 100 decided bool
108 101
109 // Pending internal channel messages. 102 // Pending internal channel messages.
110 msg chan interface{} 103 msg chan interface{}
111 104
112 // Pending user-serviceable messages. 105 // Pending user-serviceable messages.
113 » sentRequestMu sync.Mutex 106 » sentRequestMu sync.Mutex
114 » pendingRequests chan *ChannelRequest 107
108 » incomingRequests chan *ChannelRequest
115 109
116 sentEOF bool 110 sentEOF bool
117 111
118 // thread-safe data 112 // thread-safe data
119 remoteWin window 113 remoteWin window
120 pending *buffer 114 pending *buffer
121 extPending *buffer 115 extPending *buffer
122 116
123 // Protects all of the below. 117 // Protects all of the below.
124 mu sync.Mutex 118 mu sync.Mutex
(...skipping 13 matching lines...) Expand all
138 if uint32(len(packet)) > c.maxPacket { 132 if uint32(len(packet)) > c.maxPacket {
139 return fmt.Errorf("ssh: cannot write %d bytes, maxPacket is %d b ytes", len(packet), c.maxPacket) 133 return fmt.Errorf("ssh: cannot write %d bytes, maxPacket is %d b ytes", len(packet), c.maxPacket)
140 } 134 }
141 135
142 c.mu.Lock() 136 c.mu.Lock()
143 if c.sentClose { 137 if c.sentClose {
144 c.mu.Unlock() 138 c.mu.Unlock()
145 return io.EOF 139 return io.EOF
146 } 140 }
147 c.sentClose = (packet[0] == msgChannelClose) 141 c.sentClose = (packet[0] == msgChannelClose)
148 » err := c.mux.writePacket(packet) 142 » err := c.mux.conn.writePacket(packet)
149 c.mu.Unlock() 143 c.mu.Unlock()
150 return err 144 return err
151 } 145 }
152 146
153 func (c *channel) sendMessage(code byte, msg interface{}) error { 147 func (c *channel) sendMessage(code byte, msg interface{}) error {
154 if debug { 148 if debug {
155 log.Printf("send %d: %#v", c.mux.chanList.offset, msg) 149 log.Printf("send %d: %#v", c.mux.chanList.offset, msg)
156 } 150 }
157 151
158 p := marshal(code, msg) 152 p := marshal(code, msg)
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
228 return nil 222 return nil
229 } 223 }
230 data := packet[sz:] 224 data := packet[sz:]
231 if length != uint32(len(data)) { 225 if length != uint32(len(data)) {
232 return errors.New("ssh: wrong packet length") 226 return errors.New("ssh: wrong packet length")
233 } 227 }
234 228
235 c.mu.Lock() 229 c.mu.Lock()
236 if c.myWindow < length { 230 if c.myWindow < length {
237 c.mu.Unlock() 231 c.mu.Unlock()
232 // TODO(hanwen): should send Disconnect with reason?
238 return errors.New("ssh: remote side wrote too much") 233 return errors.New("ssh: remote side wrote too much")
239 } 234 }
240 c.myWindow -= length 235 c.myWindow -= length
241 c.mu.Unlock() 236 c.mu.Unlock()
242 237
243 if extended == 1 { 238 if extended == 1 {
244 c.extPending.write(data) 239 c.extPending.write(data)
245 } else if extended > 0 { 240 } else if extended > 0 {
246 // discard other extended data. 241 // discard other extended data.
247 } else { 242 } else {
248 c.pending.write(data) 243 c.pending.write(data)
249 } 244 }
250 return nil 245 return nil
251 } 246 }
252 247
253 func (c *channel) adjustWindow(n uint32) error { 248 func (c *channel) adjustWindow(n uint32) error {
254 c.mu.Lock() 249 c.mu.Lock()
255 c.myWindow += uint32(n) 250 c.myWindow += uint32(n)
256 c.mu.Unlock() 251 c.mu.Unlock()
257 return c.sendMessage(msgChannelWindowAdjust, windowAdjustMsg{ 252 return c.sendMessage(msgChannelWindowAdjust, windowAdjustMsg{
258 AdditionalBytes: uint32(n), 253 AdditionalBytes: uint32(n),
259 }) 254 })
260 } 255 }
261 256
262 func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error) { 257 func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error) {
263 if extended == 1 { 258 if extended == 1 {
264 n, err = c.extPending.Read(data) 259 n, err = c.extPending.Read(data)
265 } else if extended == 0 { 260 } else if extended == 0 {
266 n, err = c.pending.Read(data) 261 n, err = c.pending.Read(data)
262 } else {
263 return 0, fmt.Errorf("ssh: extended code %d implemented", extend ed)
267 } 264 }
268 265
269 if n > 0 { 266 if n > 0 {
270 err = c.adjustWindow(uint32(n)) 267 err = c.adjustWindow(uint32(n))
271 » » // sendWindowAdjust can return io.EOF if the remote peer has 268 » » // sendWindowAdjust can return io.EOF if the remote
272 » » // closed the connection, however we want to defer forwarding io .EOF to the 269 » » // peer has closed the connection, however we want to
273 » » // caller of Read until the buffer has been drained. 270 » » // defer forwarding io.EOF to the caller of Read until
271 » » // the buffer has been drained.
274 if n > 0 && err == io.EOF { 272 if n > 0 && err == io.EOF {
275 err = nil 273 err = nil
276 } 274 }
277 } 275 }
278 276
279 return n, err 277 return n, err
280 } 278 }
281 279
282 func (c *channel) handlePacket(packet []byte) error { 280 func (c *channel) handlePacket(packet []byte) error {
281 if uint32(len(packet)) > c.maxPacket {
282 // TODO(hanwen): should send Disconnect?
283 return errors.New("ssh: incoming packet exceeds maximum size")
284 }
285
283 switch packet[0] { 286 switch packet[0] {
284 case msgChannelData, msgChannelExtendedData: 287 case msgChannelData, msgChannelExtendedData:
285 return c.handleData(packet) 288 return c.handleData(packet)
286 case msgChannelClose: 289 case msgChannelClose:
287 // Ack the close. 290 // Ack the close.
288 c.sendMessage(msgChannelClose, channelCloseMsg{ 291 c.sendMessage(msgChannelClose, channelCloseMsg{
289 PeersId: c.remoteId}) 292 PeersId: c.remoteId})
290 293
291 c.pending.eof() 294 c.pending.eof()
292 c.extPending.eof() 295 c.extPending.eof()
293 close(c.msg) 296 close(c.msg)
294 » » close(c.pendingRequests) 297 » » close(c.incomingRequests)
295 c.mux.chanList.remove(c.localId) 298 c.mux.chanList.remove(c.localId)
296 299
297 return nil 300 return nil
298 case msgChannelEOF: 301 case msgChannelEOF:
299 c.pending.eof()
300 // RFC 4254 is mute on how EOF affects dataExt messages but 302 // RFC 4254 is mute on how EOF affects dataExt messages but
301 // it is logical to signal EOF at the same time. 303 // it is logical to signal EOF at the same time.
302 c.extPending.eof() 304 c.extPending.eof()
305
306 // For ServerConn, ChannelRequests are actually output
307 // as Read error. This means that no requests can be
308 // processed after EOF is sent, which is a bug
309 c.pending.eof()
303 return nil 310 return nil
304 } 311 }
305 312
306 decoded, err := decode(packet) 313 decoded, err := decode(packet)
307 if err != nil { 314 if err != nil {
308 return err 315 return err
309 } 316 }
310 317
311 switch msg := decoded.(type) { 318 switch msg := decoded.(type) {
312 case *windowAdjustMsg: 319 case *windowAdjustMsg:
313 if !c.remoteWin.add(msg.AdditionalBytes) { 320 if !c.remoteWin.add(msg.AdditionalBytes) {
314 return fmt.Errorf("invalid window update %d", msg.Additi onalBytes) 321 return fmt.Errorf("invalid window update %d", msg.Additi onalBytes)
315 } 322 }
316 323
317 case *channelRequestMsg: 324 case *channelRequestMsg:
318 req := ChannelRequest{ 325 req := ChannelRequest{
319 Request: msg.Request, 326 Request: msg.Request,
320 WantReply: msg.WantReply, 327 WantReply: msg.WantReply,
321 Payload: msg.RequestSpecificData, 328 Payload: msg.RequestSpecificData,
322 } 329 }
323 330
324 » » c.pendingRequests <- &req 331 » » c.incomingRequests <- &req
325 default: 332 default:
326 c.msg <- msg 333 c.msg <- msg
327 } 334 }
328 return nil 335 return nil
329 } 336 }
330 337
331 func newChannel(chanType string, extraData []byte) *channel { 338 func newChannel(chanType string, extraData []byte) *channel {
332 return &channel{ 339 return &channel{
333 » » remoteWin: window{Cond: newCond()}, 340 » » remoteWin: window{Cond: newCond()},
334 » » myWindow: defaultWindowSize, 341 » » myWindow: defaultWindowSize,
335 » » pending: newBuffer(), 342 » » pending: newBuffer(),
336 » » extPending: newBuffer(), 343 » » extPending: newBuffer(),
337 » » pendingRequests: make(chan *ChannelRequest, 16), 344 » » incomingRequests: make(chan *ChannelRequest, 16),
338 » » msg: make(chan interface{}, 16), 345 » » msg: make(chan interface{}, 16),
339 » » chanType: chanType, 346 » » chanType: chanType,
340 » » extraData: extraData, 347 » » extraData: extraData,
341 } 348 }
342 } 349 }
343 350
344 var errUndecided = errors.New("ssh: must Accept or Reject channel") 351 var errUndecided = errors.New("ssh: must Accept or Reject channel")
345 var errDecidedAlready = errors.New("ssh: can call Accept or Reject only once") 352 var errDecidedAlready = errors.New("ssh: can call Accept or Reject only once")
346 353
347 type extChannel channel 354 type extChannel struct {
355 » code uint32
356 » ch *channel
357 }
348 358
349 func (e *extChannel) Write(data []byte) (n int, err error) { 359 func (e *extChannel) Write(data []byte) (n int, err error) {
350 » return ((*channel)(e)).WriteExtended(data, 1) 360 » return e.ch.WriteExtended(data, e.code)
351 } 361 }
352 func (e *extChannel) Read(data []byte) (n int, err error) { 362 func (e *extChannel) Read(data []byte) (n int, err error) {
353 » return ((*channel)(e)).ReadExtended(data, 1) 363 » return e.ch.ReadExtended(data, e.code)
354 } 364 }
355 365
356 func (c *channel) Accept() error { 366 func (c *channel) Accept() error {
357 if c.decided { 367 if c.decided {
358 return errDecidedAlready 368 return errDecidedAlready
359 } 369 }
360 confirm := channelOpenConfirmMsg{ 370 confirm := channelOpenConfirmMsg{
361 PeersId: c.remoteId, 371 PeersId: c.remoteId,
362 MyId: c.localId, 372 MyId: c.localId,
363 MyWindow: c.myWindow, 373 MyWindow: c.myWindow,
364 MaxPacketSize: c.maxPacket, 374 MaxPacketSize: c.maxPacket,
365 } 375 }
366 c.decided = true 376 c.decided = true
367 » return c.sendMessage(msgChannelOpenConfirm, confirm) 377 » if err := c.sendMessage(msgChannelOpenConfirm, confirm); err != nil {
378 » » return err
379 » }
380
381 » return nil
368 } 382 }
369 383
370 func (ch *channel) Reject(reason RejectionReason, message string) error { 384 func (ch *channel) Reject(reason RejectionReason, message string) error {
371 if ch.decided { 385 if ch.decided {
372 return errDecidedAlready 386 return errDecidedAlready
373 } 387 }
374 reject := channelOpenFailureMsg{ 388 reject := channelOpenFailureMsg{
375 PeersId: ch.remoteId, 389 PeersId: ch.remoteId,
376 Reason: reason, 390 Reason: reason,
377 Message: message, 391 Message: message,
378 Language: "en", 392 Language: "en",
379 } 393 }
380 ch.decided = true 394 ch.decided = true
381 return ch.sendMessage(msgChannelOpenFailure, reject) 395 return ch.sendMessage(msgChannelOpenFailure, reject)
382 } 396 }
383 397
384 func (ch *channel) Read(data []byte) (int, error) { 398 func (ch *channel) Read(data []byte) (int, error) {
385 if !ch.decided { 399 if !ch.decided {
386 return 0, errUndecided 400 return 0, errUndecided
387 } 401 }
388 402
389 return ch.ReadExtended(data, 0) 403 return ch.ReadExtended(data, 0)
390 } 404 }
391 405
392 func (ch *channel) ReceivedRequests() chan *ChannelRequest {
393 return ch.pendingRequests
394 }
395
396 func (ch *channel) Write(data []byte) (int, error) { 406 func (ch *channel) Write(data []byte) (int, error) {
397 if !ch.decided { 407 if !ch.decided {
398 return 0, errUndecided 408 return 0, errUndecided
399 } 409 }
400 return ch.WriteExtended(data, 0) 410 return ch.WriteExtended(data, 0)
401 } 411 }
402 412
403 func (ch *channel) CloseWrite() error { 413 func (ch *channel) CloseWrite() error {
404 if !ch.decided { 414 if !ch.decided {
405 return errUndecided 415 return errUndecided
406 } 416 }
407 ch.sentEOF = true 417 ch.sentEOF = true
408 return ch.sendMessage(msgChannelEOF, channelEOFMsg{ 418 return ch.sendMessage(msgChannelEOF, channelEOFMsg{
409 PeersId: ch.remoteId}) 419 PeersId: ch.remoteId})
410 } 420 }
411 421
412 func (ch *channel) Close() error { 422 func (ch *channel) Close() error {
413 if !ch.decided { 423 if !ch.decided {
414 return errUndecided 424 return errUndecided
415 } 425 }
416 426
417 return ch.sendMessage(msgChannelClose, channelCloseMsg{ 427 return ch.sendMessage(msgChannelClose, channelCloseMsg{
418 PeersId: ch.remoteId}) 428 PeersId: ch.remoteId})
419 } 429 }
420 430
421 func (ch *channel) Stderr() io.ReadWriter { 431 func (ch *channel) Extended(code uint32) io.ReadWriter {
422 if !ch.decided { 432 if !ch.decided {
423 return nil 433 return nil
424 } 434 }
425 » return (*extChannel)(ch) 435 » return &extChannel{code, ch}
426 } 436 }
427 437
428 // SendRequest sends a channel request. If wantReply is set, it will 438 // SendRequest sends a channel request. If wantReply is set, it will
429 // wait for a reply and return the result as a boolean. 439 // wait for a reply and return the result as a boolean.
430 func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo l, error) { 440 func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (boo l, error) {
431 if !ch.decided { 441 if !ch.decided {
432 return false, errUndecided 442 return false, errUndecided
433 } 443 }
434 444
435 if wantReply { 445 if wantReply {
436 ch.sentRequestMu.Lock() 446 ch.sentRequestMu.Lock()
437 defer ch.sentRequestMu.Unlock() 447 defer ch.sentRequestMu.Unlock()
438 } 448 }
439 449
440 msg := channelRequestMsg{ 450 msg := channelRequestMsg{
441 PeersId: ch.remoteId, 451 PeersId: ch.remoteId,
442 Request: name, 452 Request: name,
443 WantReply: wantReply, 453 WantReply: wantReply,
444 RequestSpecificData: payload, 454 RequestSpecificData: payload,
445 } 455 }
446 456
447 if err := ch.sendMessage(msgChannelRequest, msg); err != nil { 457 if err := ch.sendMessage(msgChannelRequest, msg); err != nil {
448 return false, err 458 return false, err
449 } 459 }
450 460
451 if wantReply { 461 if wantReply {
452 » » switch (<-ch.msg).(type) { 462 » » m, ok := (<-ch.msg)
463 » » if !ok {
464 » » » return false, io.EOF
465 » » }
466 » » switch m.(type) {
453 case *channelRequestFailureMsg: 467 case *channelRequestFailureMsg:
454 return false, nil 468 return false, nil
455 case *channelRequestSuccessMsg: 469 case *channelRequestSuccessMsg:
456 return true, nil 470 return true, nil
471 default:
472 return false, fmt.Errorf("unexpected response %#v", m)
457 } 473 }
458 } 474 }
459 475
460 return false, nil 476 return false, nil
461 } 477 }
462 478
463 // AckRequest either sends an ack or nack to the channel request. 479 // AckRequest either sends an ack or nack to the channel request.
464 func (ch *channel) AckRequest(ok bool) error { 480 func (ch *channel) AckRequest(ok bool) error {
465 if !ch.decided { 481 if !ch.decided {
466 return errUndecided 482 return errUndecided
(...skipping 15 matching lines...) Expand all
482 return ch.sendMessage(code, msg) 498 return ch.sendMessage(code, msg)
483 } 499 }
484 500
485 func (ch *channel) ChannelType() string { 501 func (ch *channel) ChannelType() string {
486 return ch.chanType 502 return ch.chanType
487 } 503 }
488 504
489 func (ch *channel) ExtraData() []byte { 505 func (ch *channel) ExtraData() []byte {
490 return ch.extraData 506 return ch.extraData
491 } 507 }
508
509 // compatChannel is a hack to implement Channel's
510 // handing of channel requests.
511 type compatChannel struct {
512 *channel
513 }
514
515 func newCompatChannel(ch *channel) *compatChannel {
516 c := &compatChannel{ch}
517 go c.loop()
518 return c
519 }
520
521 func (c *compatChannel) loop() {
522 for r := range c.channel.incomingRequests {
523 c.channel.pending.addRequest(r)
524 }
525 }
526
527 func (c *compatChannel) Stderr() io.Writer {
528 return c.Extended(1)
529 }
LEFTRIGHT
« no previous file | ssh/client.go » ('j') | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

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