code-ruby 3.1.2 → 4.0.1

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