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

Delta Between Two Patch Sets: oauth/jwt/jwt_test.go

Issue 6452058: code review 6452058: goauth2: adding a jwt package to support server to serv...
Left Patch Set: Created 11 years, 8 months ago
Right Patch Set: diff -r ae891eec7124 https://code.google.com/p/goauth2/ Created 11 years, 7 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:
Right: Side by side diff | Download
« no previous file with change/comment | « oauth/jwt/jwt.go ('k') | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
(no file at all)
1 // Copyright 2012 The goauth2 Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 // For package documentation please see jwt.go.
6 //
7 package jwt
8
9 import (
10 "bytes"
11 "encoding/json"
12 "io/ioutil"
13 "net/http"
14 "testing"
15 "time"
16 )
17
18 const (
19 stdHeaderStr = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
20 iss = "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer. gserviceaccount.com"
21 scope = "https://www.googleapis.com/auth/prediction"
22 aud = "https://accounts.google.com/o/oauth2/token"
23 exp = 1328554385
24 iat = 1328550785 // exp + 1 hour
25 )
26
27 // Base64url encoded Header
28 const headerEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
29
30 // Base64url encoded ClaimSet
31 const claimSetEnc = "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2l ncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d 3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29 vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ"
32
33 // Base64url encoded Signature
34 const sigEnc = "olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7 OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8 cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3It gT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C 2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
35
36 // Base64url encoded Token
37 const tokEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjk tcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5 jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF 1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU 1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMd ZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklW lRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYc xtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtut NgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
38
39 // Private key for testing
40 const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
41 MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
42 7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
43 xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
44 SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
45 pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
46 SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
47 nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
48 HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
49 nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
50 IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
51 YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
52 Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
53 vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
54 B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
55 aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
56 eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
57 aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
58 klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
59 CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
60 UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
61 soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
62 bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
63 504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
64 YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
65 BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
66 -----END RSA PRIVATE KEY-----`
67
68 // Public key to go with the private key for testing
69 const publicKeyPem = `-----BEGIN CERTIFICATE-----
70 MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
71 BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
72 MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
73 CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
74 7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
75 uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
76 gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
77 +WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
78 ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
79 gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
80 GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
81 AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
82 odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
83 +JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
84 ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
85 ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
86 cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
87 -----END CERTIFICATE-----`
88
89 var (
90 privateKeyPemBytes = []byte(privateKeyPem)
91 publicKeyPemBytes = []byte(publicKeyPem)
92 )
93
94 // The signature in bytes
95 var sigBytes = Signature{162, 91, 164, 108, 122, 222, 54, 38, 43, 130, 33, 130,
96 76, 73, 152, 221, 229, 134, 121, 59, 216, 13, 37, 7, 98, 129, 60, 224,
97 156, 247, 5, 19, 193, 73, 163, 29, 100, 195, 78, 159, 253, 2, 96, 174,
98 212, 28, 243, 187, 57, 219, 212, 161, 248, 240, 126, 221, 93, 31, 159,
99 84, 196, 79, 70, 89, 45, 54, 166, 49, 109, 139, 91, 128, 66, 130, 38,
100 106, 168, 203, 102, 132, 215, 175, 202, 162, 23, 163, 191, 144, 54, 189,
101 36, 218, 50, 146, 73, 86, 149, 16, 8, 192, 98, 25, 144, 59, 169, 9, 47,
102 188, 113, 48, 39, 51, 249, 44, 75, 41, 162, 31, 87, 40, 40, 154, 203,
103 13, 69, 255, 4, 205, 49, 217, 254, 34, 48, 84, 51, 132, 190, 111, 79,
104 89, 101, 249, 152, 169, 39, 73, 77, 151, 22, 249, 66, 204, 151, 196, 54,
105 28, 198, 209, 216, 130, 225, 117, 219, 235, 233, 191, 114, 45, 129, 61,
106 18, 77, 247, 23, 161, 108, 200, 25, 10, 4, 110, 28, 1, 245, 32, 105,
107 244, 150, 28, 67, 198, 32, 207, 170, 88, 14, 57, 180, 173, 101, 253, 46,
108 183, 33, 153, 60, 131, 2, 176, 168, 118, 146, 139, 22, 219, 173, 54, 7,
109 8, 21, 67, 253, 125, 108, 111, 193, 221, 66, 217, 211, 112, 228, 104,
110 203, 112, 170, 253, 107, 244, 128, 17, 28, 168, 39, 101, 167, 9, 29,
111 127, 143, 211, 116, 192, 61, 168, 210}
112
113 // Testing the urlEncode function.
114 func TestUrlEncode(t *testing.T) {
115 enc := urlEncode([]byte(stdHeaderStr))
116 b := []byte(enc)
117 if b[len(b)-1] == 61 {
118 t.Error("TestUrlEncode: last chat == \"=\"")
119 }
120 if enc != headerEnc {
121 t.Error("TestUrlEncode: enc != headerEnc")
122 t.Errorf(" enc = %s", enc)
123 t.Errorf(" headerEnc = %s", headerEnc)
124 }
125 }
126
127 // Given a well formed Header, test for proper encoding.
128 func TestHeaderEncode(t *testing.T) {
129 h := &Header{
130 Algorithm: "RS256",
131 Type: "JWT",
132 }
133 enc, err := h.Encode()
134 if err != nil {
135 t.Errorf("TestHeaderEncode:h.Encode: %v", err)
136 }
137 if enc != headerEnc {
138 t.Error("TestHeaderEncode: enc != headerEnc")
139 t.Errorf(" enc = %s", enc)
140 t.Errorf(" headerEnc = %s", headerEnc)
141 }
142 }
143
144 // Test that the times are set properly.
145 func TestClaimSetSetTimes(t *testing.T) {
146 c := &ClaimSet{
147 Iss: iss,
148 Scope: scope,
149 Aud: aud,
150 }
151 iatTime := time.Unix(iat, 0)
152 c.setTimes(iatTime)
153 if c.Exp != exp {
154 t.Error("TestClaimSetSetTimes: c.Exp != exp")
155 t.Errorf(" c.Exp = %d", c.Exp)
156 t.Errorf(" exp = %d", exp)
157 }
158 }
159
160 // Given a well formed ClaimSet, test for proper encoding.
161 func TestClaimSetEncode(t *testing.T) {
162 c := &ClaimSet{
163 Iss: iss,
164 Scope: scope,
165 Aud: aud,
166 Exp: exp,
167 Iat: iat,
168 }
169 enc, err := c.Encode()
170 if err != nil {
171 t.Errorf("TestClaimSetEncode:c.Encode: %v", err)
172 }
173 if enc != claimSetEnc {
174 t.Error("TestClaimSetEncode: enc != claimSetEnc")
175 t.Errorf(" enc = %s", enc)
176 t.Errorf(" claimSetEnc = %s", claimSetEnc)
177 }
178 }
179
180 // Given a well formed Signature, test for proper encoding.
181 func TestSignatureEncode(t *testing.T) {
182 enc, err := sigBytes.Encode()
183 if err != nil {
184 t.Errorf("TestSignatureEncode:sigBytes.Encode: %v", err)
185 }
186 if enc != sigEnc {
187 t.Error("TestSignatureEncode: enc != sigEnc")
188 t.Errorf(" enc = %s", enc)
189 t.Errorf(" sigEnc = %s", sigEnc)
190 }
191 }
192
193 // Test the NewToken constructor.
194 func TestNewToken(t *testing.T) {
195 tok := NewToken(iss, scope, aud, privateKeyPemBytes)
196 if tok.Header != StdHeader {
197 t.Fail()
198 }
199 if tok.ClaimSet.Iss != iss {
200 t.Error("TestNewToken: tok.ClaimSet.Iss != iss")
201 t.Errorf(" tok.ClaimSet.Iss = %s", tok.ClaimSet.Iss)
202 t.Errorf(" iss = %s", iss)
203 }
204 if tok.ClaimSet.Scope != scope {
205 t.Error("TestNewToken: tok.ClaimSet.Scope != scope")
206 t.Errorf(" tok.ClaimSet.Scope = %s", tok.ClaimSet.Scope)
207 t.Errorf(" scope = %s", scope)
208 }
209 if tok.ClaimSet.Aud != aud {
210 t.Error("TestNewToken: tok.ClaimSet.Aud != aud")
211 t.Errorf(" tok.ClaimSet.Aud = %s", tok.ClaimSet.Aud)
212 t.Errorf(" aud = %s", aud)
213 }
214 if !bytes.Equal(tok.Key, privateKeyPemBytes) {
215 t.Error("TestNewToken: tok.Key != privateKeyPemBytes")
216 t.Errorf(" tok.Key = %s", tok.Key)
217 t.Errorf(" privateKeyPemBytes = %s", privateKeyPemBytes)
218 }
219 }
220
221 // Make sure the private key parsing functions work.
222 func TestParsePrivateKey(t *testing.T) {
223 tok := &Token{
224 Key: privateKeyPemBytes,
225 }
226 err := tok.parsePrivateKey()
227 if err != nil {
228 t.Errorf("TestParsePrivateKey:tok.parsePrivateKey: %v", err)
229 }
230 }
231
232 // Test that the token signature generated matches the golden standard.
233 func TestTokenSign(t *testing.T) {
234 tok := &Token{
235 Key: privateKeyPemBytes,
236 head: headerEnc,
237 claim: claimSetEnc,
238 }
239 err := tok.parsePrivateKey()
240 if err != nil {
241 t.Errorf("TestTokenSign:tok.parsePrivateKey: %v", err)
242 }
243 err = tok.sign()
244 if err != nil {
245 t.Errorf("TestTokenSign:tok.sign: %v", err)
246 }
247 if len(tok.Signature) != len(sigBytes) {
248 t.Error("TestTokenSign: len(tok.Signature) != len(sigBytes)")
249 t.Errorf(" len(tok.Signature) = %d", len(tok.Signature))
250 t.Errorf(" len(sigBytes) = %d", len(sigBytes))
251 }
252 if !bytes.Equal(tok.Signature, sigBytes) {
253 t.Error("TestTokenSign: tok.Signature != sigBytes")
254 t.Errorf(" tok.Signature = %s", tok.Signature)
255 t.Errorf(" sigBytes = %s", sigBytes)
256 }
257 }
258
259 // Test that the token expiration function is working.
260 func TestTokenExpired(t *testing.T) {
261 c := &ClaimSet{}
262 tok := &Token{
263 ClaimSet: c,
264 }
265 now := time.Now()
266 c.setTimes(now)
267 if tok.Expired() != false {
268 t.Error("TestTokenExpired: tok.Expired != false")
269 }
270 // Set the times as if they were set 2 hours ago.
271 c.setTimes(now.Add(-2 * time.Hour))
272 if tok.Expired() != true {
273 t.Error("TestTokenExpired: tok.Expired != true")
274 }
275 }
276
277 // Given a well formed Token, test for proper encoding.
278 func TestTokenEncode(t *testing.T) {
279 c := &ClaimSet{
280 Iss: iss,
281 Scope: scope,
282 Aud: aud,
283 Exp: exp,
284 Iat: iat,
285 }
286 tok := &Token{
287 Header: StdHeader,
288 ClaimSet: c,
289 Key: privateKeyPemBytes,
290 }
291 enc, err := tok.Encode()
292 if err != nil {
293 t.Errorf("TestTokenEncode:tok.Assertion: %v", err)
294 }
295 if enc != tokEnc {
296 t.Error("TestTokenEncode: enc != tokEnc")
297 t.Errorf(" enc = %s", enc)
298 t.Errorf(" tokEnc = %s", tokEnc)
299 }
300 }
301
302 // Given a well formed Token we should get back a well formed request.
303 func TestBuildRequest(t *testing.T) {
304 c := &ClaimSet{
305 Iss: iss,
306 Scope: scope,
307 Aud: aud,
308 Exp: exp,
309 Iat: iat,
310 }
311 tok := &Token{
312 Header: StdHeader,
313 ClaimSet: c,
314 Key: privateKeyPemBytes,
315 }
316 u, v, err := tok.buildRequest()
317 if err != nil {
318 t.Errorf("TestBuildRequest:BuildRequest: %v", err)
319 }
320 if u != c.Aud {
321 t.Error("TestBuildRequest: u != c.Aud")
322 t.Errorf(" u = %s", u)
323 t.Errorf(" c.Aud = %s", c.Aud)
324 }
325 if v.Get("grant_type") != stdGrantType {
326 t.Error("TestBuildRequest: grant_type != stdGrantType")
327 t.Errorf(" grant_type = %s", v.Get("grant_type"))
328 t.Errorf(" stdGrantType = %s", stdGrantType)
329 }
330 if v.Get("assertion_type") != tok.AssertionType() {
331 t.Error("TestBuildRequest: assertion_type != t.AssertionType")
332 t.Errorf(" assertion_type = %s", v.Get("assertion_type"))
333 t.Errorf(" t.AssertionType = %s", tok.AssertionType())
334 }
335 if v.Get("assertion") != tokEnc {
336 t.Error("TestBuildRequest: assertion != tokEnc")
337 t.Errorf(" assertion = %s", v.Get("assertion"))
338 t.Errorf(" tokEnc = %s", tokEnc)
339 }
340 }
341
342 // Given a well formed access request response we should get back a oauth.Token.
343 func TestHandleResponse(t *testing.T) {
344 rb := &respBody{
345 Access: "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
346 Type: "Bearer",
347 ExpiresIn: 3600,
348 }
349 b, err := json.Marshal(rb)
350 if err != nil {
351 t.Errorf("TestHandleResponse:json.Marshal: %v", err)
352 }
353 r := &http.Response{
354 Status: "200 OK",
355 StatusCode: 200,
356 Body: ioutil.NopCloser(bytes.NewReader(b)),
357 }
358 o, err := handleResponse(r)
359 if err != nil {
360 t.Errorf("TestHandleResponse:handleResponse: %v", err)
361 }
362 if o.AccessToken != rb.Access {
363 t.Error("TestHandleResponse: o.AccessToken != rb.Access")
364 t.Errorf(" o.AccessToken = %s", o.AccessToken)
365 t.Errorf(" rb.Access = %s", rb.Access)
366 }
367 if o.Expired() {
368 t.Error("TestHandleResponse: o.Expired == true")
369 }
370 }
371
372 // Placeholder for future Assert tests.
373 func TestAssert(t *testing.T) {
374 // Since this method makes a call to BuildRequest, an htttp.Client, and
375 // finally HandleResponse there is not much more to test. This is here
376 // as a placeholder if that changes.
377 }
378
379 // Benchmark for the end-to-end encoding of a well formed token.
380 func BenchmarkTokenEncode(b *testing.B) {
381 b.StopTimer()
382 c := &ClaimSet{
383 Iss: iss,
384 Scope: scope,
385 Aud: aud,
386 Exp: exp,
387 Iat: iat,
388 }
389 tok := &Token{
390 Header: StdHeader,
391 ClaimSet: c,
392 Key: privateKeyPemBytes,
393 }
394 b.StartTimer()
395 for i := 0; i < b.N; i++ {
396 c.Exp = 0
397 tok.Encode()
398 }
399 }
LEFTRIGHT

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