code-ruby 3.1.1 → 4.0.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/code +97 -20
  4. data/lib/code/concerns/shared.rb +331 -15
  5. data/lib/code/format.rb +22 -2
  6. data/lib/code/network.rb +87 -0
  7. data/lib/code/node/call.rb +79 -2
  8. data/lib/code/node/call_argument.rb +14 -0
  9. data/lib/code/node/code.rb +5 -4
  10. data/lib/code/node/function_parameter.rb +7 -4
  11. data/lib/code/node/list.rb +29 -1
  12. data/lib/code/node/while.rb +21 -0
  13. data/lib/code/object/base_64.rb +132 -6
  14. data/lib/code/object/boolean.rb +60 -0
  15. data/lib/code/object/class.rb +138 -2
  16. data/lib/code/object/code.rb +111 -3
  17. data/lib/code/object/context.rb +57 -1
  18. data/lib/code/object/cryptography.rb +63 -0
  19. data/lib/code/object/date.rb +13339 -462
  20. data/lib/code/object/decimal.rb +1725 -0
  21. data/lib/code/object/dictionary.rb +1835 -12
  22. data/lib/code/object/duration.rb +28 -0
  23. data/lib/code/object/function.rb +261 -23
  24. data/lib/code/object/global.rb +534 -1
  25. data/lib/code/object/html.rb +227 -16
  26. data/lib/code/object/http.rb +244 -14
  27. data/lib/code/object/ics.rb +75 -13
  28. data/lib/code/object/identifier_list.rb +17 -2
  29. data/lib/code/object/integer.rb +1941 -2
  30. data/lib/code/object/json.rb +75 -1
  31. data/lib/code/object/list.rb +3417 -10
  32. data/lib/code/object/nothing.rb +53 -0
  33. data/lib/code/object/number.rb +110 -0
  34. data/lib/code/object/parameter.rb +140 -0
  35. data/lib/code/object/range.rb +596 -14
  36. data/lib/code/object/smtp.rb +95 -12
  37. data/lib/code/object/string.rb +944 -3
  38. data/lib/code/object/super.rb +10 -1
  39. data/lib/code/object/time.rb +13358 -498
  40. data/lib/code/object/url.rb +65 -0
  41. data/lib/code/object.rb +543 -0
  42. data/lib/code/parser.rb +177 -26
  43. data/lib/code-ruby.rb +3 -0
  44. data/lib/code.rb +30 -3
  45. metadata +135 -84
  46. data/.github/dependabot.yml +0 -15
  47. data/.github/workflows/ci.yml +0 -38
  48. data/.gitignore +0 -30
  49. data/.node-version +0 -1
  50. data/.npm-version +0 -1
  51. data/.prettierignore +0 -2
  52. data/.rspec +0 -1
  53. data/.rubocop.yml +0 -140
  54. data/.ruby-version +0 -1
  55. data/.tool-versions +0 -3
  56. data/AGENTS.md +0 -43
  57. data/Gemfile +0 -22
  58. data/Gemfile.lock +0 -292
  59. data/Rakefile +0 -5
  60. data/bin/bundle +0 -123
  61. data/bin/bundle-audit +0 -31
  62. data/bin/bundler-audit +0 -31
  63. data/bin/dorian +0 -31
  64. data/bin/rspec +0 -31
  65. data/bin/rubocop +0 -31
  66. data/bin/test +0 -5
  67. data/code-ruby.gemspec +0 -34
  68. data/docs/precedence.txt +0 -36
  69. data/package-lock.json +0 -14
  70. data/package.json +0 -7
  71. data/spec/bin/code_spec.rb +0 -48
  72. data/spec/code/format_spec.rb +0 -153
  73. data/spec/code/node/call_spec.rb +0 -11
  74. data/spec/code/object/boolean_spec.rb +0 -18
  75. data/spec/code/object/cryptography_spec.rb +0 -25
  76. data/spec/code/object/decimal_spec.rb +0 -50
  77. data/spec/code/object/dictionary_spec.rb +0 -98
  78. data/spec/code/object/function_spec.rb +0 -268
  79. data/spec/code/object/http_spec.rb +0 -33
  80. data/spec/code/object/ics_spec.rb +0 -50
  81. data/spec/code/object/integer_spec.rb +0 -42
  82. data/spec/code/object/list_spec.rb +0 -22
  83. data/spec/code/object/nothing_spec.rb +0 -14
  84. data/spec/code/object/range_spec.rb +0 -23
  85. data/spec/code/object/string_spec.rb +0 -26
  86. data/spec/code/parser/boolean_spec.rb +0 -11
  87. data/spec/code/parser/chained_call_spec.rb +0 -16
  88. data/spec/code/parser/dictionary_spec.rb +0 -18
  89. data/spec/code/parser/function_spec.rb +0 -16
  90. data/spec/code/parser/group_spec.rb +0 -11
  91. data/spec/code/parser/if_modifier_spec.rb +0 -18
  92. data/spec/code/parser/list_spec.rb +0 -17
  93. data/spec/code/parser/number_spec.rb +0 -11
  94. data/spec/code/parser/string_spec.rb +0 -20
  95. data/spec/code/parser_spec.rb +0 -52
  96. data/spec/code/type_spec.rb +0 -21
  97. data/spec/code_spec.rb +0 -642
  98. data/spec/spec_helper.rb +0 -21
  99. data/spec/zeitwerk/loader_spec.rb +0 -7
@@ -3,6 +3,144 @@
3
3
  class Code
4
4
  class Object
5
5
  class Html < Object
6
+ CLASS_DOCUMENTATION = {
7
+ name: "Html",
8
+ description: "builds, escapes, parses, and queries html fragments.",
9
+ examples: [
10
+ "Html.p { :hello }",
11
+ "Html.escape(\"<p>a</p>\")",
12
+ "Html.raw(\"<p>a</p>\").css(\"p\")"
13
+ ]
14
+ }.freeze
15
+ CLASS_FUNCTIONS = {
16
+ "escape" => {
17
+ name: "escape",
18
+ description: "escapes a value for html text.",
19
+ examples: [
20
+ "Html.escape(\"<p>\")",
21
+ "Html.escape(:hello)",
22
+ "Html.escape(() => { \"<strong>hello</strong>\" })"
23
+ ]
24
+ },
25
+ "unescape" => {
26
+ name: "unescape",
27
+ description: "converts html entities and tags to text.",
28
+ examples: [
29
+ "Html.unescape(\"&lt;p&gt;\")",
30
+ "Html.unescape(\"<p>hello</p>\")",
31
+ "Html.unescape(() => { \"&amp;\" })"
32
+ ]
33
+ },
34
+ "join" => {
35
+ name: "join",
36
+ description: "joins html or text values with an optional separator.",
37
+ examples: [
38
+ "Html.join([:a, :b], \", \")",
39
+ "Html.join([Html.text(\"a\"), Html.text(\"b\")])",
40
+ "Html.join(\"<br>\", () => { [\"a\", \"b\"] })"
41
+ ]
42
+ },
43
+ "text" => {
44
+ name: "text",
45
+ description: "builds an escaped html text fragment.",
46
+ examples: [
47
+ "Html.text(:hello)",
48
+ "Html.text(\"<strong>hello</strong>\")",
49
+ "Html.text(() => { :hello })"
50
+ ]
51
+ },
52
+ "raw" => {
53
+ name: "raw",
54
+ description: "parses markup into an html fragment without escaping text.",
55
+ examples: [
56
+ "Html.raw(\"<strong>hello</strong>\")",
57
+ "Html.raw(Html.text(:hello))",
58
+ "Html.raw(() => { \"<em>hello</em>\" })"
59
+ ]
60
+ }
61
+ }.freeze
62
+ INSTANCE_FUNCTIONS = {
63
+ "css" => {
64
+ name: "css",
65
+ description: "returns all html nodes matching a css selector.",
66
+ examples: [
67
+ "Html.raw(\"<p>a</p><p>b</p>\").css(\"p\")",
68
+ "Html.raw(\"<main><p>a</p></main>\").css(\"main p\")",
69
+ "Html.raw(\"<p class='x'>a</p>\").css(\".x\")"
70
+ ]
71
+ },
72
+ "at_css" => {
73
+ name: "at_css",
74
+ description: "returns the first html node matching a css selector.",
75
+ examples: [
76
+ "Html.raw(\"<p>a</p><p>b</p>\").at_css(\"p\")",
77
+ "Html.raw(\"<main><p>a</p></main>\").at_css(\"main p\")",
78
+ "Html.raw(\"<p class='x'>a</p>\").at_css(\".x\")"
79
+ ]
80
+ },
81
+ "map" => {
82
+ name: "map",
83
+ description: "returns a list by calling a function for each html node.",
84
+ examples: [
85
+ "Html.raw(\"<p>a</p><p>b</p>\").css(\"p\").map((node) => { node.to_string })",
86
+ "Html.raw(\"<p>a</p>\").css(:p).map((node, index) => { index })",
87
+ "Html.raw(\"<p>a</p>\").css(:p).map((node, index, nodes) => { nodes.to_string })"
88
+ ]
89
+ },
90
+ "to_string" => {
91
+ name: "to_string",
92
+ description: "returns the html fragment's text content.",
93
+ examples: [
94
+ "Html.raw(\"<p>hello</p>\").to_string",
95
+ "Html.text(:hello).to_string",
96
+ "Html.raw(\"<p><strong>hello</strong></p>\").to_string"
97
+ ]
98
+ },
99
+ "to_html" => {
100
+ name: "to_html",
101
+ description: "returns the html fragment as html markup.",
102
+ examples: [
103
+ "Html.raw(\"<p>hello</p>\").to_html",
104
+ "Html.text(:p).to_html",
105
+ "Html.raw(\"<strong>hello</strong>\").to_html"
106
+ ]
107
+ },
108
+ "inner_text" => {
109
+ name: "inner_text",
110
+ description: "returns the html fragment's text content.",
111
+ examples: [
112
+ "Html.raw(\"<p>hello</p>\").inner_text",
113
+ "Html.text(:hello).inner_text",
114
+ "Html.raw(\"<p><strong>hello</strong></p>\").inner_text"
115
+ ]
116
+ },
117
+ "attribute" => {
118
+ name: "attribute",
119
+ description: "returns an html node attribute by name.",
120
+ examples: [
121
+ "Html.raw(\"<a href='/'>home</a>\").at_css(:a).attribute(:href)",
122
+ "Html.raw(\"<p class='x'>a</p>\").at_css(:p).attribute(:class)",
123
+ "Html.raw(\"<img alt='logo'>\").at_css(:img).attribute(:alt)"
124
+ ]
125
+ },
126
+ "attributes" => {
127
+ name: "attributes",
128
+ description: "returns an html node's attributes as a dictionary.",
129
+ examples: [
130
+ "Html.raw(\"<a href='/'>home</a>\").at_css(:a).attributes",
131
+ "Html.raw(\"<p class='x'>a</p>\").at_css(:p).attributes",
132
+ "Html.raw(\"<img alt='logo'>\").at_css(:img).attributes"
133
+ ]
134
+ }
135
+ }.freeze
136
+
137
+ def self.function_documentation(scope)
138
+ return INSTANCE_FUNCTIONS if scope == :instance
139
+ return CLASS_FUNCTIONS if scope == :class
140
+
141
+ {}
142
+ end
143
+
6
144
  TAGS = %w[
7
145
  a
8
146
  abbr
@@ -124,7 +262,9 @@ class Code
124
262
  args.first.is_a?(Nokogiri::XML::Node)
125
263
  args.first
126
264
  else
127
- Nokogiri.HTML(args.first.to_s)
265
+ source = args.first.to_s
266
+ ::Code.ensure_input_size!(source, label: "html")
267
+ Nokogiri.HTML(source)
128
268
  end
129
269
  end
130
270
 
@@ -188,14 +328,18 @@ class Code
188
328
 
189
329
  if code_function.something?
190
330
  code_content =
191
- code_function.call(
192
- arguments: List.new([code_name, code_attributes]),
193
- **globals
194
- )
331
+ begin
332
+ code_function.call(
333
+ arguments: List.new([code_name, code_attributes]),
334
+ **globals
335
+ )
336
+ rescue Error::Next => e
337
+ e.code_value
338
+ end
195
339
 
196
340
  content =
197
341
  if code_content.is_an?(Html)
198
- Nokogiri::HTML::DocumentFragment.parse(code_content.to_html)
342
+ fragment_from_html(code_content)
199
343
  else
200
344
  Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
201
345
  end
@@ -206,33 +350,55 @@ class Code
206
350
  fragment.add_child(node)
207
351
 
208
352
  Html.new(fragment)
353
+ rescue Error::Break => e
354
+ e.code_value
209
355
  end
210
356
 
211
357
  def self.code_escape(value_or_function = nil, **globals)
212
358
  code_value =
213
359
  if value_or_function.is_a?(Function)
214
- value_or_function.to_code.call(**globals)
360
+ begin
361
+ value_or_function.to_code.call(**globals)
362
+ rescue Error::Next => e
363
+ e.code_value
364
+ end
215
365
  else
216
366
  value_or_function.to_code
217
367
  end
218
368
 
219
369
  String.new(CGI.escapeHTML(code_value.to_s))
370
+ rescue Error::Break => e
371
+ e.code_value
220
372
  end
221
373
 
222
374
  def self.code_unescape(value_or_function = nil, **globals)
223
375
  code_value =
224
376
  if value_or_function.is_a?(Function)
225
- value_or_function.to_code.call(**globals)
377
+ begin
378
+ value_or_function.to_code.call(**globals)
379
+ rescue Error::Next => e
380
+ e.code_value
381
+ end
226
382
  else
227
383
  value_or_function.to_code
228
384
  end
229
385
 
230
- String.new(Nokogiri::HTML.fragment(code_value.to_s).text)
386
+ source = code_value.to_s
387
+ ::Code.ensure_input_size!(source, label: "html")
388
+
389
+ String.new(Nokogiri::HTML.fragment(source).text)
390
+ rescue Error::Break => e
391
+ e.code_value
231
392
  end
232
393
 
233
394
  def self.code_join(first = nil, second = nil, **globals)
234
395
  if second.is_a?(Function)
235
- code_contents = second.to_code.call(**globals)
396
+ code_contents =
397
+ begin
398
+ second.to_code.call(**globals)
399
+ rescue Error::Next => e
400
+ e.code_value
401
+ end
236
402
  code_separator = first.to_code
237
403
  else
238
404
  code_contents = first.to_code
@@ -247,14 +413,14 @@ class Code
247
413
  code_contents.raw.each.with_index do |code_content, index|
248
414
  content =
249
415
  if code_content.is_an?(Html)
250
- Nokogiri::HTML::DocumentFragment.parse(code_content.to_html)
416
+ fragment_from_html(code_content)
251
417
  else
252
418
  Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
253
419
  end
254
420
 
255
421
  separator =
256
422
  if code_separator.is_an?(Html)
257
- Nokogiri::HTML::DocumentFragment.parse(code_separator.to_html)
423
+ fragment_from_html(code_separator)
258
424
  else
259
425
  Nokogiri::XML::Text.new(code_separator.to_s, fragment.document)
260
426
  end
@@ -264,12 +430,18 @@ class Code
264
430
  end
265
431
 
266
432
  Html.new(fragment)
433
+ rescue Error::Break => e
434
+ e.code_value
267
435
  end
268
436
 
269
437
  def self.code_text(value_or_function = nil, **globals)
270
438
  code_value =
271
439
  if value_or_function.is_a?(Function)
272
- value_or_function.to_code.call(**globals)
440
+ begin
441
+ value_or_function.to_code.call(**globals)
442
+ rescue Error::Next => e
443
+ e.code_value
444
+ end
273
445
  else
274
446
  value_or_function.to_code
275
447
  end
@@ -280,21 +452,37 @@ class Code
280
452
  )
281
453
 
282
454
  Html.new(fragment)
455
+ rescue Error::Break => e
456
+ e.code_value
283
457
  end
284
458
 
285
459
  def self.code_raw(value_or_function = nil, **globals)
286
460
  code_value =
287
461
  if value_or_function.is_a?(Function)
288
- value_or_function.to_code.call(**globals)
462
+ begin
463
+ value_or_function.to_code.call(**globals)
464
+ rescue Error::Next => e
465
+ e.code_value
466
+ end
289
467
  else
290
468
  value_or_function.to_code
291
469
  end
292
470
 
293
471
  if code_value.is_an?(Html)
294
- Html.new(Nokogiri::HTML::DocumentFragment.parse(code_value.to_html))
472
+ Html.new(fragment_from_html(code_value))
295
473
  else
296
- Html.new(Nokogiri::HTML::DocumentFragment.parse(code_value.to_s))
474
+ source = code_value.to_s
475
+ ::Code.ensure_input_size!(source, label: "html")
476
+ Html.new(Nokogiri::HTML::DocumentFragment.parse(source))
297
477
  end
478
+ rescue Error::Break => e
479
+ e.code_value
480
+ end
481
+
482
+ def self.fragment_from_html(html)
483
+ source = html.to_html
484
+ ::Code.ensure_input_size!(source, label: "html")
485
+ Nokogiri::HTML::DocumentFragment.parse(source)
298
486
  end
299
487
 
300
488
  def call(**args)
@@ -319,9 +507,15 @@ class Code
319
507
  when "to_html"
320
508
  sig(args)
321
509
  code_to_html
510
+ when "inner_text"
511
+ sig(args)
512
+ code_inner_text
322
513
  when "attribute"
323
514
  sig(args) { String }
324
515
  code_attribute(code_value)
516
+ when "attributes"
517
+ sig(args)
518
+ code_attributes
325
519
  else
326
520
  super
327
521
  end
@@ -352,6 +546,8 @@ class Code
352
546
  e.code_value
353
547
  end
354
548
  )
549
+ rescue Error::Break => e
550
+ e.code_value
355
551
  end
356
552
 
357
553
  def to_s
@@ -370,10 +566,25 @@ class Code
370
566
  String.new(raw.text)
371
567
  end
372
568
 
569
+ def code_inner_text
570
+ code_to_string
571
+ end
572
+
373
573
  def code_attribute(value = nil)
374
574
  code_value = value.to_code
375
575
  String.new(raw.attr(code_value.to_s))
376
576
  end
577
+
578
+ def code_attributes
579
+ node = raw.is_a?(::Nokogiri::XML::NodeSet) ? raw.first : raw
580
+ return Dictionary.new if node.blank?
581
+
582
+ Dictionary.new(
583
+ node.attribute_nodes.to_h do |attribute|
584
+ [attribute.name.to_code, attribute.value.to_code]
585
+ end
586
+ )
587
+ end
377
588
  end
378
589
  end
379
590
  end
@@ -3,6 +3,105 @@
3
3
  class Code
4
4
  class Object
5
5
  class Http < Object
6
+ CLASS_DOCUMENTATION = {
7
+ name: "Http",
8
+ description: "sends http requests and returns dictionaries with request and response details.",
9
+ examples: [
10
+ "Http.get(\"http://httpbin.org/status/200\").success?",
11
+ "Http.post(\"http://httpbin.org/status/200\", body: :hello).request.body",
12
+ "Http.fetch(:get, \"http://httpbin.org/status/204\").code"
13
+ ]
14
+ }.freeze
15
+ CLASS_FUNCTIONS = {
16
+ "get" => {
17
+ name: "get",
18
+ description: "sends a get request and returns the response dictionary.",
19
+ examples: [
20
+ "Http.get(\"http://httpbin.org/status/200\").code",
21
+ "Http.get(\"http://httpbin.org/status/200\", query: { q: :ruby }).url",
22
+ "Http.get(\"http://httpbin.org/status/200\", headers: { accept: \"application/json\" }).request.headers"
23
+ ]
24
+ },
25
+ "head" => {
26
+ name: "head",
27
+ description: "sends a head request and returns the response dictionary.",
28
+ examples: [
29
+ "Http.head(\"http://httpbin.org/status/200\").code",
30
+ "Http.head(\"http://httpbin.org/status/204\").status",
31
+ "Http.head(\"http://httpbin.org/status/200\", headers: { accept: \"text/html\" }).request.headers"
32
+ ]
33
+ },
34
+ "post" => {
35
+ name: "post",
36
+ description: "sends a post request with optional body, form data, and headers.",
37
+ examples: [
38
+ "Http.post(\"http://httpbin.org/status/200\", body: :hello).request.body",
39
+ "Http.post(\"http://httpbin.org/status/200\", data: { a: 1 }).success?",
40
+ "Http.post(\"http://httpbin.org/status/200\", headers: { content_type: \"text/plain\" }).request.headers"
41
+ ]
42
+ },
43
+ "put" => {
44
+ name: "put",
45
+ description: "sends a put request with optional body or form data.",
46
+ examples: [
47
+ "Http.put(\"http://httpbin.org/status/200\", body: :hello).request.body",
48
+ "Http.put(\"http://httpbin.org/status/200\", data: { a: 1 }).method",
49
+ "Http.put(\"http://httpbin.org/status/204\").code"
50
+ ]
51
+ },
52
+ "delete" => {
53
+ name: "delete",
54
+ description: "sends a delete request and returns the response dictionary.",
55
+ examples: [
56
+ "Http.delete(\"http://httpbin.org/status/200\").success?",
57
+ "Http.delete(\"http://httpbin.org/status/200\", body: :hello).request.body",
58
+ "Http.delete(\"http://httpbin.org/status/204\").status"
59
+ ]
60
+ },
61
+ "options" => {
62
+ name: "options",
63
+ description: "sends an options request and returns the response dictionary.",
64
+ examples: [
65
+ "Http.options(\"http://httpbin.org/status/200\").method",
66
+ "Http.options(\"http://httpbin.org/status/204\").code",
67
+ "Http.options(\"http://httpbin.org/status/200\", headers: { accept: \"*/*\" }).request.headers"
68
+ ]
69
+ },
70
+ "trace" => {
71
+ name: "trace",
72
+ description: "sends a trace request and returns the response dictionary.",
73
+ examples: [
74
+ "Http.trace(\"http://httpbin.org/status/200\").method",
75
+ "Http.trace(\"http://httpbin.org/status/204\").code",
76
+ "Http.trace(\"http://httpbin.org/status/200\", headers: { accept: \"message/http\" }).request.headers"
77
+ ]
78
+ },
79
+ "patch" => {
80
+ name: "patch",
81
+ description: "sends a patch request with optional body or form data.",
82
+ examples: [
83
+ "Http.patch(\"http://httpbin.org/status/200\", body: :hello).request.body",
84
+ "Http.patch(\"http://httpbin.org/status/200\", data: { a: 1 }).method",
85
+ "Http.patch(\"http://httpbin.org/status/204\").code"
86
+ ]
87
+ },
88
+ "fetch" => {
89
+ name: "fetch",
90
+ description: "sends an http request using the given method name.",
91
+ examples: [
92
+ "Http.fetch(:get, \"http://httpbin.org/status/200\").success?",
93
+ "Http.fetch(:post, \"http://httpbin.org/status/200\", body: :hello).request.body",
94
+ "Http.fetch(:patch, \"http://httpbin.org/status/200\", data: { a: 1 }).method"
95
+ ]
96
+ }
97
+ }.freeze
98
+
99
+ def self.function_documentation(scope)
100
+ return CLASS_FUNCTIONS if scope == :class
101
+
102
+ {}
103
+ end
104
+
6
105
  SIG = [
7
106
  String,
8
107
  {
@@ -85,6 +184,20 @@ class Code
85
184
  network_authentication_required: 511
86
185
  }.freeze
87
186
  DEFAULT_TIMEOUT = 1.hour.to_f
187
+ MAX_REQUEST_BYTES = ::Code::MAX_INPUT_BYTES
188
+ MAX_RESPONSE_BYTES = ::Code::MAX_INPUT_BYTES
189
+ MAX_HEADER_BYTES = 32.kilobytes
190
+ HEADER_NAME = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
191
+ RESTRICTED_HEADERS = %w[
192
+ connection
193
+ content-length
194
+ host
195
+ proxy-authorization
196
+ te
197
+ trailer
198
+ transfer-encoding
199
+ upgrade
200
+ ].freeze
88
201
 
89
202
  def self.call(**args)
90
203
  code_operator = args.fetch(:operator, nil).to_code
@@ -163,7 +276,7 @@ class Code
163
276
  username = options.code_get("username").to_s
164
277
  password = options.code_get("password").to_s
165
278
  body = options.code_get("body").to_s
166
- headers = options.code_get("headers").raw || {}
279
+ headers = sanitized_headers(options.code_get("headers").raw || {})
167
280
  data = options.code_get("data").raw || {}
168
281
  timeout = options.code_get("timeout")
169
282
  open_timeout = options.code_get("open_timeout")
@@ -171,31 +284,39 @@ class Code
171
284
  write_timeout = options.code_get("write_timeout")
172
285
  query = options.code_get("query").raw || {}
173
286
  query = query.to_a.flatten.map(&:to_s).each_slice(2).to_h.to_query
287
+ validate_payload_size!(username, label: "http username")
288
+ validate_payload_size!(password, label: "http password")
289
+ validate_payload_size!(query, label: "http query")
290
+ validate_payload_size!(body, label: "http request body")
291
+ validate_payload_size!(data.as_json.to_query, label: "http form data") if data.present?
174
292
 
175
293
  url = original_url
176
294
  url = "#{url}?#{query}" if query.present?
295
+ validate_payload_size!(url, label: "http url")
177
296
 
178
297
  if username.present? || password.present?
179
298
  authorization = ::Base64.strict_encode64("#{username}:#{password}")
180
299
  headers["Authorization"] = "Basic #{authorization}"
300
+ validate_header_size!("Authorization", headers["Authorization"])
181
301
  end
182
302
 
183
- uri = ::URI.parse(url)
184
- http = ::Net::HTTP.new(uri.host, uri.port)
303
+ uri = parse_uri(url)
304
+ resolved_ip = ::Code::Network.validate_public_uri!(uri, service: "http")
305
+
306
+ http = ::Net::HTTP.new(uri.hostname, uri.port, nil)
307
+ http.ipaddr = resolved_ip
185
308
  http.use_ssl = true if uri.scheme == "https"
186
- default_timeout = timeout.nothing? ? DEFAULT_TIMEOUT : timeout.to_f
309
+ default_timeout = http_timeout(timeout, DEFAULT_TIMEOUT)
187
310
  open_timeout_value =
188
- open_timeout.nothing? ? default_timeout : open_timeout.to_f
311
+ http_timeout(open_timeout, default_timeout)
189
312
  read_timeout_value =
190
- read_timeout.nothing? ? default_timeout : read_timeout.to_f
313
+ http_timeout(read_timeout, default_timeout)
191
314
  write_timeout_value =
192
- write_timeout.nothing? ? default_timeout : write_timeout.to_f
315
+ http_timeout(write_timeout, default_timeout)
193
316
 
194
317
  http.open_timeout = open_timeout_value if open_timeout_value
195
318
  http.read_timeout = read_timeout_value if read_timeout_value
196
- if http.respond_to?(:write_timeout=) && write_timeout_value
197
- http.write_timeout = write_timeout_value
198
- end
319
+ http.write_timeout = write_timeout_value if write_timeout_value
199
320
 
200
321
  request_class =
201
322
  case verb
@@ -223,7 +344,19 @@ class Code
223
344
  request.set_form_data(**data.as_json) if data.present?
224
345
 
225
346
  begin
226
- response = http.request(request)
347
+ response_body = +""
348
+ response =
349
+ http.request(request) do |http_response|
350
+ validate_response_headers!(http_response)
351
+
352
+ http_response.read_body do |chunk|
353
+ if response_body.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
354
+ raise ::Code::Error, "http response is too large"
355
+ end
356
+
357
+ response_body << chunk
358
+ end
359
+ end
227
360
  rescue ::Timeout::Error, ::Errno::ETIMEDOUT
228
361
  raise ::Code::Error, "http timeout"
229
362
  rescue OpenSSL::SSL::SSLError, IOError, SystemCallError, SocketError
@@ -234,9 +367,10 @@ class Code
234
367
  location = response["location"].to_s
235
368
 
236
369
  if (300..399).cover?(code) && location.present? && redirects.positive?
237
- new_uri = ::URI.join(uri, location)
370
+ new_uri = parse_uri(::URI.join(uri, location).to_s)
371
+ ::Code::Network.validate_public_uri!(new_uri, service: "http")
238
372
 
239
- if new_uri.host == uri.host
373
+ if same_origin?(new_uri, uri)
240
374
  code_fetch(
241
375
  "get",
242
376
  new_uri.to_s,
@@ -248,8 +382,104 @@ class Code
248
382
  end
249
383
  else
250
384
  status = STATUS_CODES.key(code) || :ok
251
- Dictionary.new(code: code, status: status, body: response.body.to_s)
385
+ response_headers = response.each_header.to_h
386
+ request_headers = request.to_hash.transform_values do |values|
387
+ List.new(values)
388
+ end
389
+ body = response_body.to_s
390
+
391
+ Dictionary.new(
392
+ code: code,
393
+ status: status,
394
+ body: body,
395
+ headers: response_headers,
396
+ method: verb,
397
+ url: url,
398
+ "success?" => code.between?(200, 299),
399
+ "redirect?" => code.between?(300, 399),
400
+ request: {
401
+ method: verb,
402
+ url: url,
403
+ headers: request_headers,
404
+ body: request.body.to_s
405
+ },
406
+ response: {
407
+ code: code,
408
+ status: status,
409
+ headers: response_headers,
410
+ body: body
411
+ }
412
+ )
413
+ end
414
+ end
415
+
416
+ def self.parse_uri(url)
417
+ ::URI.parse(url)
418
+ rescue ::URI::InvalidURIError
419
+ raise ::Code::Error, "http: invalid url"
420
+ end
421
+
422
+ def self.same_origin?(new_uri, original_uri)
423
+ new_uri.scheme == original_uri.scheme &&
424
+ new_uri.hostname.to_s.downcase == original_uri.hostname.to_s.downcase &&
425
+ new_uri.port == original_uri.port
426
+ end
427
+
428
+ def self.sanitized_headers(headers)
429
+ headers.to_h.to_h do |key, value|
430
+ name = key.to_s
431
+ header_value = value.to_s
432
+
433
+ validate_header_name!(name)
434
+ validate_header_value!(header_value)
435
+ validate_header_size!(name, header_value)
436
+
437
+ [name, header_value]
438
+ end
439
+ end
440
+
441
+ def self.validate_header_name!(name)
442
+ normalized_name = name.downcase
443
+
444
+ unless HEADER_NAME.match?(name)
445
+ raise ::Code::Error, "http: invalid header name"
252
446
  end
447
+
448
+ return unless RESTRICTED_HEADERS.include?(normalized_name)
449
+
450
+ raise ::Code::Error, "http: restricted header #{name.inspect}"
451
+ end
452
+
453
+ def self.validate_header_value!(value)
454
+ return unless /[\r\n\0]/.match?(value)
455
+
456
+ raise ::Code::Error, "http: invalid header value"
457
+ end
458
+
459
+ def self.validate_header_size!(name, value)
460
+ return if name.bytesize + value.bytesize <= MAX_HEADER_BYTES
461
+
462
+ raise ::Code::Error, "http: request headers are too large"
463
+ end
464
+
465
+ def self.validate_response_headers!(response)
466
+ size =
467
+ response.each_header.sum do |name, value|
468
+ name.to_s.bytesize + value.to_s.bytesize
469
+ end
470
+ return if size <= MAX_HEADER_BYTES
471
+
472
+ raise ::Code::Error, "http response headers are too large"
473
+ end
474
+
475
+ def self.validate_payload_size!(value, label:)
476
+ return if value.to_s.bytesize <= MAX_REQUEST_BYTES
477
+
478
+ raise ::Code::Error, "#{label} is too large"
479
+ end
480
+
481
+ def self.http_timeout(value, default)
482
+ ::Code.normalize_timeout!(value.nothing? ? default : value)
253
483
  end
254
484
  end
255
485
  end