syntropy 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/TODO.md +0 -39
- data/cmd/serve.rb +0 -2
- data/cmd/test.rb +76 -20
- data/examples/blog/app/posts/[id]/index.rb +4 -1
- data/examples/blog/app/posts/index.rb +3 -1
- data/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- data/lib/syntropy/http/server_connection.rb +15 -9
- data/lib/syntropy/module_loader.rb +5 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/session.rb +113 -0
- data/lib/syntropy/request.rb +9 -0
- data/lib/syntropy/test.rb +72 -1
- data/lib/syntropy/version.rb +1 -1
- data/syntropy.gemspec +1 -0
- data/test/test_http_server_connection.rb +8 -5
- data/test/test_request_session.rb +254 -0
- metadata +17 -2
- data/examples/mcp-oauth/test/helper.rb +0 -9
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'helper'
|
|
4
3
|
require 'base64'
|
|
5
4
|
require 'digest'
|
|
6
5
|
|
|
7
|
-
class OAuthBaseTest <
|
|
8
|
-
APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
|
|
9
|
-
HTTP = Syntropy::HTTP
|
|
10
|
-
|
|
6
|
+
class OAuthBaseTest < Syntropy::Test
|
|
11
7
|
def setup
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
root_dir: APP_ROOT,
|
|
15
|
-
mount_path: '/',
|
|
16
|
-
machine: @machine
|
|
17
|
-
)
|
|
18
|
-
@store = @app.module_loader.load('_lib/auth_store')
|
|
19
|
-
@test_harness = Syntropy::TestHarness.new(@app)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def teardown
|
|
23
|
-
@machine = nil
|
|
24
|
-
@app = nil
|
|
25
|
-
@test_harness = nil
|
|
8
|
+
super
|
|
9
|
+
@store = load_module('_lib/auth_store')
|
|
26
10
|
end
|
|
27
11
|
end
|
|
28
12
|
|
|
@@ -42,18 +26,15 @@ end
|
|
|
42
26
|
|
|
43
27
|
class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
44
28
|
def test_mcp_no_bearer_token
|
|
45
|
-
req =
|
|
29
|
+
req = post_json(
|
|
30
|
+
'/mcp',
|
|
46
31
|
{
|
|
47
|
-
':method' => 'POST',
|
|
48
|
-
':path' => '/mcp',
|
|
49
|
-
'content-type' => 'application/json'
|
|
50
|
-
},
|
|
51
|
-
JSON.dump({
|
|
52
32
|
method: 'initialize',
|
|
53
33
|
jsonrpc: '2.0',
|
|
54
34
|
params: {}
|
|
55
|
-
}
|
|
35
|
+
}
|
|
56
36
|
)
|
|
37
|
+
|
|
57
38
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
58
39
|
|
|
59
40
|
www_auth = req.response_headers['WWW-Authenticate']
|
|
@@ -62,18 +43,14 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
62
43
|
end
|
|
63
44
|
|
|
64
45
|
def test_mcp_invalid_bearer_token
|
|
65
|
-
req =
|
|
46
|
+
req = post_json(
|
|
47
|
+
'/mcp',
|
|
66
48
|
{
|
|
67
|
-
':method' => 'POST',
|
|
68
|
-
':path' => '/mcp',
|
|
69
|
-
'content-type' => 'application/json',
|
|
70
|
-
'authorization' => 'Bearer foobar'
|
|
71
|
-
},
|
|
72
|
-
JSON.dump({
|
|
73
49
|
method: 'initialize',
|
|
74
50
|
jsonrpc: '2.0',
|
|
75
51
|
params: {}
|
|
76
|
-
}
|
|
52
|
+
},
|
|
53
|
+
'authorization' => 'Bearer foobar'
|
|
77
54
|
)
|
|
78
55
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
79
56
|
|
|
@@ -83,10 +60,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
83
60
|
end
|
|
84
61
|
|
|
85
62
|
def test_oauth_protected_resource_metadatas
|
|
86
|
-
req =
|
|
87
|
-
':method' => 'GET',
|
|
88
|
-
':path' => '/.well-known/oauth-protected-resource'
|
|
89
|
-
)
|
|
63
|
+
req = get('/.well-known/oauth-protected-resource')
|
|
90
64
|
assert_equal HTTP::OK, req.response_status
|
|
91
65
|
json = req.response_json
|
|
92
66
|
assert_equal ["http://localhost:1234/"], json['authorization_servers']
|
|
@@ -94,10 +68,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
94
68
|
end
|
|
95
69
|
|
|
96
70
|
def test_oauth_authorization_server_metadata
|
|
97
|
-
req =
|
|
98
|
-
':method' => 'GET',
|
|
99
|
-
':path' => '/.well-known/oauth-authorization-server'
|
|
100
|
-
)
|
|
71
|
+
req = get('/.well-known/oauth-authorization-server')
|
|
101
72
|
assert_equal HTTP::OK, req.response_status
|
|
102
73
|
json = req.response_json
|
|
103
74
|
assert_equal "http://localhost:1234/", json['issuer']
|
|
@@ -117,14 +88,10 @@ class OAuthPhase2ClientRegistrationTest < OAuthBaseTest
|
|
|
117
88
|
"grant_types" => ["authorization_code", "refresh_token"]
|
|
118
89
|
}
|
|
119
90
|
|
|
120
|
-
req =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'content-type' => 'application/json',
|
|
125
|
-
'authorization' => 'Bearer foobar'
|
|
126
|
-
},
|
|
127
|
-
JSON.dump(client_info)
|
|
91
|
+
req = post_json(
|
|
92
|
+
'/oauth/register',
|
|
93
|
+
client_info,
|
|
94
|
+
'authorization' => 'Bearer foobar'
|
|
128
95
|
)
|
|
129
96
|
|
|
130
97
|
assert_equal HTTP::CREATED, req.response_status
|
|
@@ -148,14 +115,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
148
115
|
"redirect_uris" => ["http://localhost:8400/callback"],
|
|
149
116
|
"grant_types" => ["authorization_code", "refresh_token"]
|
|
150
117
|
}
|
|
151
|
-
req =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'content-type' => 'application/json',
|
|
156
|
-
'authorization' => 'Bearer foobar'
|
|
157
|
-
},
|
|
158
|
-
JSON.dump(client_info)
|
|
118
|
+
req = post_json(
|
|
119
|
+
'/oauth/register',
|
|
120
|
+
client_info,
|
|
121
|
+
'authorization' => 'Bearer foobar'
|
|
159
122
|
)
|
|
160
123
|
assert_equal HTTP::CREATED, req.response_status
|
|
161
124
|
json = req.response_json
|
|
@@ -169,10 +132,7 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
169
132
|
'code_challenge_method' => 'S256',
|
|
170
133
|
'state' => 'my_state'
|
|
171
134
|
}
|
|
172
|
-
req =
|
|
173
|
-
':method' => 'GET',
|
|
174
|
-
':path' => "/oauth/authorize?#{URI.encode_www_form(params)}"
|
|
175
|
-
)
|
|
135
|
+
req = get("/oauth/authorize?#{URI.encode_www_form(params)}")
|
|
176
136
|
assert_equal HTTP::FOUND, req.response_status
|
|
177
137
|
assert_equal '/signin', req.response_headers['Location']
|
|
178
138
|
|
|
@@ -202,17 +162,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
202
162
|
}
|
|
203
163
|
oauth_signin_id = @store.store(auth_params)
|
|
204
164
|
|
|
205
|
-
req =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
210
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
211
|
-
},
|
|
212
|
-
URI.encode_www_form(
|
|
213
|
-
username: 'foobar',
|
|
214
|
-
password: 'foobar'
|
|
215
|
-
)
|
|
165
|
+
req = post_form(
|
|
166
|
+
'/signin',
|
|
167
|
+
{ username: 'foobar', password: 'foobar' },
|
|
168
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
216
169
|
)
|
|
217
170
|
|
|
218
171
|
auth_info = @store.fetch(oauth_signin_id)
|
|
@@ -227,27 +180,15 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
227
180
|
end
|
|
228
181
|
|
|
229
182
|
def test_signin_endpoint_get
|
|
230
|
-
req =
|
|
231
|
-
{
|
|
232
|
-
':method' => 'GET',
|
|
233
|
-
':path' => '/signin',
|
|
234
|
-
}
|
|
235
|
-
)
|
|
183
|
+
req = get('/signin')
|
|
236
184
|
assert_equal HTTP::OK, req.response_status
|
|
237
185
|
assert_equal 'text/html', req.response_content_type
|
|
238
186
|
end
|
|
239
187
|
|
|
240
188
|
def test_signin_endpoint_post_bad_creds
|
|
241
|
-
req =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
':path' => '/signin',
|
|
245
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
246
|
-
},
|
|
247
|
-
URI.encode_www_form(
|
|
248
|
-
username: 'foobar',
|
|
249
|
-
password: 'bad'
|
|
250
|
-
)
|
|
189
|
+
req = post_form(
|
|
190
|
+
'/signin',
|
|
191
|
+
{ username: 'foobar', password: 'bad' }
|
|
251
192
|
)
|
|
252
193
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
253
194
|
assert_equal 'text/html', req.response_content_type
|
|
@@ -255,16 +196,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
255
196
|
end
|
|
256
197
|
|
|
257
198
|
def test_signin_endpoint_post_good_creds
|
|
258
|
-
req =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
':path' => '/signin',
|
|
262
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
263
|
-
},
|
|
264
|
-
URI.encode_www_form(
|
|
265
|
-
username: 'foobar',
|
|
266
|
-
password: 'foobar'
|
|
267
|
-
)
|
|
199
|
+
req = post_form(
|
|
200
|
+
'/signin',
|
|
201
|
+
{ username: 'foobar', password: 'foobar' }
|
|
268
202
|
)
|
|
269
203
|
assert_equal HTTP::SEE_OTHER, req.response_status
|
|
270
204
|
assert_equal '/', req.response_headers['Location']
|
|
@@ -277,22 +211,14 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
277
211
|
end
|
|
278
212
|
|
|
279
213
|
def test_oauth_consent_endpoint_get_no_oauth_signin_id
|
|
280
|
-
req =
|
|
281
|
-
{
|
|
282
|
-
':method' => 'GET',
|
|
283
|
-
':path' => '/oauth/consent',
|
|
284
|
-
}
|
|
285
|
-
)
|
|
214
|
+
req = get('/oauth/consent')
|
|
286
215
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
287
216
|
end
|
|
288
217
|
|
|
289
218
|
def test_oauth_consent_endpoint_get_invalid_oauth_signin_id
|
|
290
|
-
req =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
':path' => '/oauth/consent',
|
|
294
|
-
'cookie' => 'outh_signin_id=foo'
|
|
295
|
-
}
|
|
219
|
+
req = get(
|
|
220
|
+
'/oauth/consent',
|
|
221
|
+
'cookie' => 'outh_signin_id=foo'
|
|
296
222
|
)
|
|
297
223
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
298
224
|
end
|
|
@@ -321,12 +247,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
321
247
|
}
|
|
322
248
|
oauth_signin_id = @store.store(auth_params)
|
|
323
249
|
|
|
324
|
-
req =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
':path' => '/oauth/consent',
|
|
328
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
329
|
-
}
|
|
250
|
+
req = get(
|
|
251
|
+
'/oauth/consent',
|
|
252
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
330
253
|
)
|
|
331
254
|
assert_equal HTTP::OK, req.response_status
|
|
332
255
|
end
|
|
@@ -355,16 +278,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
355
278
|
}
|
|
356
279
|
oauth_signin_id = @store.store(auth_params)
|
|
357
280
|
|
|
358
|
-
req =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
363
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
364
|
-
},
|
|
365
|
-
URI.encode_www_form(
|
|
366
|
-
decision: 'deny',
|
|
367
|
-
)
|
|
281
|
+
req = post_form(
|
|
282
|
+
'/oauth/consent',
|
|
283
|
+
{ decision: 'deny' },
|
|
284
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
368
285
|
)
|
|
369
286
|
assert_equal HTTP::FOUND, req.response_status
|
|
370
287
|
|
|
@@ -396,16 +313,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
396
313
|
}
|
|
397
314
|
oauth_signin_id = @store.store(auth_params)
|
|
398
315
|
|
|
399
|
-
req =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
404
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
405
|
-
},
|
|
406
|
-
URI.encode_www_form(
|
|
407
|
-
decision: 'allow',
|
|
408
|
-
)
|
|
316
|
+
req = post_form(
|
|
317
|
+
'/oauth/consent',
|
|
318
|
+
{ decision: 'allow' },
|
|
319
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
409
320
|
)
|
|
410
321
|
assert_equal HTTP::FOUND, req.response_status
|
|
411
322
|
|
|
@@ -452,19 +363,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
452
363
|
end
|
|
453
364
|
|
|
454
365
|
def test_oauth_token_exchange
|
|
455
|
-
req =
|
|
366
|
+
req = post_form(
|
|
367
|
+
'/oauth/token',
|
|
456
368
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
'
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
code: @auth_code,
|
|
464
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
465
|
-
client_id: @client_id,
|
|
466
|
-
code_verifier: @code_verifier
|
|
467
|
-
)
|
|
369
|
+
grant_type: 'authorization_code',
|
|
370
|
+
code: @auth_code,
|
|
371
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
372
|
+
client_id: @client_id,
|
|
373
|
+
code_verifier: @code_verifier
|
|
374
|
+
}
|
|
468
375
|
)
|
|
469
376
|
|
|
470
377
|
assert_equal HTTP::OK, req.response_status
|
|
@@ -483,19 +390,8 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
483
390
|
end
|
|
484
391
|
|
|
485
392
|
def test_oauth_token_exchange_missing_params
|
|
486
|
-
req =
|
|
487
|
-
{
|
|
488
|
-
':method' => 'POST',
|
|
489
|
-
':path' => '/oauth/token',
|
|
490
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
491
|
-
},
|
|
492
|
-
URI.encode_www_form({}
|
|
493
|
-
# grant_type: 'authorization_code',
|
|
494
|
-
# code: auth_code,
|
|
495
|
-
# redirect_uri: 'http://localhost:4321/callback',
|
|
496
|
-
# client_id: client_id,
|
|
497
|
-
# code_verifier: code_verifier
|
|
498
|
-
)
|
|
393
|
+
req = post_form(
|
|
394
|
+
'/oauth/token', {}
|
|
499
395
|
)
|
|
500
396
|
|
|
501
397
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -507,19 +403,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
507
403
|
end
|
|
508
404
|
|
|
509
405
|
def test_oauth_token_exchange_invalid_grant_type
|
|
510
|
-
req =
|
|
406
|
+
req = post_form(
|
|
407
|
+
'/oauth/token',
|
|
511
408
|
{
|
|
512
|
-
':method' => 'POST',
|
|
513
|
-
':path' => '/oauth/token',
|
|
514
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
515
|
-
},
|
|
516
|
-
URI.encode_www_form(
|
|
517
409
|
grant_type: 'foo',
|
|
518
410
|
code: @auth_code,
|
|
519
411
|
redirect_uri: 'http://localhost:4321/callback',
|
|
520
412
|
client_id: @client_id,
|
|
521
413
|
code_verifier: @code_verifier
|
|
522
|
-
|
|
414
|
+
}
|
|
523
415
|
)
|
|
524
416
|
|
|
525
417
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -531,19 +423,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
531
423
|
end
|
|
532
424
|
|
|
533
425
|
def test_oauth_token_exchange_invalid_code
|
|
534
|
-
req =
|
|
426
|
+
req = post_form(
|
|
427
|
+
'/oauth/token',
|
|
535
428
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
'
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
code: @auth_code + '!',
|
|
543
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
544
|
-
client_id: @client_id,
|
|
545
|
-
code_verifier: @code_verifier
|
|
546
|
-
)
|
|
429
|
+
grant_type: 'authorization_code',
|
|
430
|
+
code: @auth_code + '!',
|
|
431
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
432
|
+
client_id: @client_id,
|
|
433
|
+
code_verifier: @code_verifier
|
|
434
|
+
}
|
|
547
435
|
)
|
|
548
436
|
|
|
549
437
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -555,19 +443,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
555
443
|
end
|
|
556
444
|
|
|
557
445
|
def test_oauth_token_exchange_invalid_redirect_uri
|
|
558
|
-
req =
|
|
446
|
+
req = post_form(
|
|
447
|
+
'/oauth/token',
|
|
559
448
|
{
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
'
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
code: @auth_code,
|
|
567
|
-
redirect_uri: 'http://localhost:4321/foo',
|
|
568
|
-
client_id: @client_id,
|
|
569
|
-
code_verifier: @code_verifier
|
|
570
|
-
)
|
|
449
|
+
grant_type: 'authorization_code',
|
|
450
|
+
code: @auth_code,
|
|
451
|
+
redirect_uri: 'http://localhost:4321/foo',
|
|
452
|
+
client_id: @client_id,
|
|
453
|
+
code_verifier: @code_verifier
|
|
454
|
+
}
|
|
571
455
|
)
|
|
572
456
|
|
|
573
457
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -579,19 +463,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
579
463
|
end
|
|
580
464
|
|
|
581
465
|
def test_oauth_token_exchange_invalid_client_id
|
|
582
|
-
req =
|
|
466
|
+
req = post_form(
|
|
467
|
+
'/oauth/token',
|
|
583
468
|
{
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
'
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
code: @auth_code + '!',
|
|
591
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
592
|
-
client_id: @client_id + 'foo',
|
|
593
|
-
code_verifier: @code_verifier
|
|
594
|
-
)
|
|
469
|
+
grant_type: 'authorization_code',
|
|
470
|
+
code: @auth_code + '!',
|
|
471
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
472
|
+
client_id: @client_id + 'foo',
|
|
473
|
+
code_verifier: @code_verifier
|
|
474
|
+
}
|
|
595
475
|
)
|
|
596
476
|
|
|
597
477
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -603,19 +483,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
603
483
|
end
|
|
604
484
|
|
|
605
485
|
def test_oauth_token_exchange_invalid_code_verifier
|
|
606
|
-
req =
|
|
486
|
+
req = post_form(
|
|
487
|
+
'/oauth/token',
|
|
607
488
|
{
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
'
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
code: @auth_code + '!',
|
|
615
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
616
|
-
client_id: @client_id,
|
|
617
|
-
code_verifier: @code_verifier + 'abc'
|
|
618
|
-
)
|
|
489
|
+
grant_type: 'authorization_code',
|
|
490
|
+
code: @auth_code + '!',
|
|
491
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
492
|
+
client_id: @client_id,
|
|
493
|
+
code_verifier: @code_verifier + 'abc'
|
|
494
|
+
}
|
|
619
495
|
)
|
|
620
496
|
|
|
621
497
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -23,12 +23,11 @@ module Syntropy
|
|
|
23
23
|
|
|
24
24
|
@done = nil
|
|
25
25
|
@response_headers = nil
|
|
26
|
+
@response_cookies = nil
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def run
|
|
29
30
|
loop do
|
|
30
|
-
@done = nil
|
|
31
|
-
@response_headers = nil
|
|
32
31
|
persist = serve_request
|
|
33
32
|
break if !persist
|
|
34
33
|
end
|
|
@@ -47,6 +46,9 @@ module Syntropy
|
|
|
47
46
|
# object and handing it off to the app handler. Returns true if the
|
|
48
47
|
# connection should be persisted.
|
|
49
48
|
def serve_request
|
|
49
|
+
@done = nil
|
|
50
|
+
@response_headers = nil
|
|
51
|
+
@response_cookies = nil
|
|
50
52
|
@closed = nil
|
|
51
53
|
headers = @io.http_read_request_headers
|
|
52
54
|
return false if !headers
|
|
@@ -130,13 +132,10 @@ module Syntropy
|
|
|
130
132
|
@response_headers ? @response_headers.merge!(headers) : @response_headers = headers
|
|
131
133
|
end
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
else
|
|
138
|
-
set_response_headers('Set-Cookie' => cookies)
|
|
139
|
-
end
|
|
135
|
+
DELETE_COOKIE = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly"
|
|
136
|
+
|
|
137
|
+
def set_cookie(key, value)
|
|
138
|
+
(@response_cookies ||= {})[key] = value || DELETE_COOKIE
|
|
140
139
|
end
|
|
141
140
|
|
|
142
141
|
SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
|
|
@@ -152,6 +151,7 @@ module Syntropy
|
|
|
152
151
|
# @param body [String] response body
|
|
153
152
|
# @param headers
|
|
154
153
|
def respond(request, body, headers)
|
|
154
|
+
add_set_cookie_headers if @response_cookies
|
|
155
155
|
headers = @response_headers.merge(headers) if @response_headers
|
|
156
156
|
|
|
157
157
|
formatted_headers = format_headers(headers, body)
|
|
@@ -315,6 +315,12 @@ module Syntropy
|
|
|
315
315
|
lines << "#{key}: #{value}\r\n"
|
|
316
316
|
end
|
|
317
317
|
end
|
|
318
|
+
|
|
319
|
+
def add_set_cookie_headers
|
|
320
|
+
@response_headers ||= {}
|
|
321
|
+
sc = (@response_headers['Set-Cookie'] ||= [])
|
|
322
|
+
@response_cookies.each { |k, v| sc << "#{k}=#{v}" }
|
|
323
|
+
end
|
|
318
324
|
end
|
|
319
325
|
end
|
|
320
326
|
end
|
|
@@ -39,11 +39,12 @@ module Syntropy
|
|
|
39
39
|
# @return [any] export value
|
|
40
40
|
def load(ref, raise_on_missing: true)
|
|
41
41
|
ref = "/#{ref}" if ref !~ /^\//
|
|
42
|
+
if !(entry = @modules[ref])
|
|
43
|
+
entry = load_module(ref, raise_on_missing:)
|
|
44
|
+
return if !entry
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@modules[ref] ||= entry
|
|
46
|
+
@modules[ref] = entry
|
|
47
|
+
end
|
|
47
48
|
entry[:export_value]
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module Syntropy
|
|
8
|
+
class Session
|
|
9
|
+
def initialize(request)
|
|
10
|
+
@request = request
|
|
11
|
+
@data = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](key)
|
|
15
|
+
@data ||= load
|
|
16
|
+
@data[key]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def []=(key, value)
|
|
20
|
+
@data ||= load
|
|
21
|
+
@data[key] = value
|
|
22
|
+
save(@data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(key)
|
|
26
|
+
@data ||= load
|
|
27
|
+
@data.delete(key)
|
|
28
|
+
save(@data.empty? ? nil : @data)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def discard
|
|
32
|
+
save(nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flash
|
|
36
|
+
@data ||= load
|
|
37
|
+
@flash ||= Flash.new(self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
class NowFlash
|
|
43
|
+
def initialize
|
|
44
|
+
@data = {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def [](key)
|
|
48
|
+
@data[key.to_s]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def []=(key, value)
|
|
52
|
+
@data[key.to_s] = value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each(&block)
|
|
56
|
+
@data.each { |k, v| block.(k.to_sym, v) }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class Flash
|
|
61
|
+
def initialize(session)
|
|
62
|
+
@session = session
|
|
63
|
+
@current_flash_data = @session['_flash']
|
|
64
|
+
@session.delete('_flash') if @current_flash_data
|
|
65
|
+
@current_flash_data ||= {}
|
|
66
|
+
@future_flash_data = {}
|
|
67
|
+
@now_flash_data = NowFlash.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def [](key)
|
|
71
|
+
key = key.to_s
|
|
72
|
+
@now_flash_data[key] || @current_flash_data[key]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def []=(key, value)
|
|
76
|
+
key = key.to_s
|
|
77
|
+
@future_flash_data[key] = value
|
|
78
|
+
@session['_flash'] = @future_flash_data
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def each(&block)
|
|
82
|
+
@now_flash_data.each { |k, v| block.(k.to_sym, v) }
|
|
83
|
+
@current_flash_data.each_pair { |k, v| block.(k.to_sym, v) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def keep
|
|
87
|
+
@future_flash_data = @current_flash_data.merge!(@future_flash_data)
|
|
88
|
+
@session['_flash'] = @future_flash_data
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def now
|
|
92
|
+
@now_flash_data
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Loads session data from
|
|
97
|
+
def load
|
|
98
|
+
data = @request.cookies['__syntropy_session__']
|
|
99
|
+
return {} if !data
|
|
100
|
+
|
|
101
|
+
JSON.parse(Base64.decode64(data))
|
|
102
|
+
rescue JSON::ParserError
|
|
103
|
+
{}
|
|
104
|
+
ensure
|
|
105
|
+
@loaded = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def save(data)
|
|
109
|
+
cookie = data ? "#{Base64.strict_encode64(JSON.dump(data))}; Path=/; HttpOnly" : nil
|
|
110
|
+
@request.set_cookie('__syntropy_session__', cookie)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|