code-ruby 3.1.2 → 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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/code +97 -20
- data/lib/code/concerns/shared.rb +331 -15
- data/lib/code/format.rb +15 -1
- data/lib/code/network.rb +87 -0
- data/lib/code/node/call.rb +79 -2
- data/lib/code/node/call_argument.rb +14 -0
- data/lib/code/node/code.rb +5 -4
- data/lib/code/node/function_parameter.rb +7 -4
- data/lib/code/node/list.rb +29 -1
- data/lib/code/object/base_64.rb +132 -6
- data/lib/code/object/boolean.rb +60 -0
- data/lib/code/object/class.rb +138 -2
- data/lib/code/object/code.rb +111 -3
- data/lib/code/object/context.rb +57 -1
- data/lib/code/object/cryptography.rb +63 -0
- data/lib/code/object/date.rb +13339 -462
- data/lib/code/object/decimal.rb +1725 -0
- data/lib/code/object/dictionary.rb +1790 -11
- data/lib/code/object/duration.rb +28 -0
- data/lib/code/object/function.rb +261 -23
- data/lib/code/object/global.rb +534 -1
- data/lib/code/object/html.rb +179 -7
- data/lib/code/object/http.rb +244 -14
- data/lib/code/object/ics.rb +75 -13
- data/lib/code/object/identifier_list.rb +17 -2
- data/lib/code/object/integer.rb +1937 -2
- data/lib/code/object/json.rb +75 -1
- data/lib/code/object/list.rb +3383 -10
- data/lib/code/object/nothing.rb +53 -0
- data/lib/code/object/number.rb +110 -0
- data/lib/code/object/parameter.rb +140 -0
- data/lib/code/object/range.rb +576 -14
- data/lib/code/object/smtp.rb +95 -12
- data/lib/code/object/string.rb +944 -3
- data/lib/code/object/super.rb +10 -1
- data/lib/code/object/time.rb +13358 -498
- data/lib/code/object/url.rb +65 -0
- data/lib/code/object.rb +543 -0
- data/lib/code/parser.rb +161 -24
- data/lib/code-ruby.rb +3 -0
- data/lib/code.rb +30 -3
- metadata +135 -84
- data/.github/dependabot.yml +0 -15
- data/.github/workflows/ci.yml +0 -38
- data/.gitignore +0 -30
- data/.node-version +0 -1
- data/.npm-version +0 -1
- data/.prettierignore +0 -2
- data/.rspec +0 -1
- data/.rubocop.yml +0 -140
- data/.ruby-version +0 -1
- data/.tool-versions +0 -3
- data/AGENTS.md +0 -43
- data/Gemfile +0 -22
- data/Gemfile.lock +0 -292
- data/Rakefile +0 -5
- data/bin/bundle +0 -123
- data/bin/bundle-audit +0 -31
- data/bin/bundler-audit +0 -31
- data/bin/dorian +0 -31
- data/bin/rspec +0 -31
- data/bin/rubocop +0 -31
- data/bin/test +0 -5
- data/code-ruby.gemspec +0 -34
- data/docs/precedence.txt +0 -36
- data/package-lock.json +0 -14
- data/package.json +0 -7
- data/spec/bin/code_spec.rb +0 -48
- data/spec/code/format_spec.rb +0 -153
- data/spec/code/node/call_spec.rb +0 -11
- data/spec/code/object/boolean_spec.rb +0 -18
- data/spec/code/object/cryptography_spec.rb +0 -25
- data/spec/code/object/decimal_spec.rb +0 -50
- data/spec/code/object/dictionary_spec.rb +0 -98
- data/spec/code/object/function_spec.rb +0 -268
- data/spec/code/object/http_spec.rb +0 -33
- data/spec/code/object/ics_spec.rb +0 -50
- data/spec/code/object/integer_spec.rb +0 -42
- data/spec/code/object/list_spec.rb +0 -22
- data/spec/code/object/nothing_spec.rb +0 -14
- data/spec/code/object/range_spec.rb +0 -23
- data/spec/code/object/string_spec.rb +0 -26
- data/spec/code/parser/boolean_spec.rb +0 -11
- data/spec/code/parser/chained_call_spec.rb +0 -16
- data/spec/code/parser/dictionary_spec.rb +0 -18
- data/spec/code/parser/function_spec.rb +0 -16
- data/spec/code/parser/group_spec.rb +0 -11
- data/spec/code/parser/if_modifier_spec.rb +0 -18
- data/spec/code/parser/list_spec.rb +0 -17
- data/spec/code/parser/number_spec.rb +0 -11
- data/spec/code/parser/string_spec.rb +0 -20
- data/spec/code/parser_spec.rb +0 -52
- data/spec/code/type_spec.rb +0 -21
- data/spec/code_spec.rb +0 -717
- data/spec/spec_helper.rb +0 -21
- data/spec/zeitwerk/loader_spec.rb +0 -7
data/lib/code/object/html.rb
CHANGED
|
@@ -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(\"<p>\")",
|
|
30
|
+
"Html.unescape(\"<p>hello</p>\")",
|
|
31
|
+
"Html.unescape(() => { \"&\" })"
|
|
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
|
-
|
|
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
|
|
|
@@ -199,7 +339,7 @@ class Code
|
|
|
199
339
|
|
|
200
340
|
content =
|
|
201
341
|
if code_content.is_an?(Html)
|
|
202
|
-
|
|
342
|
+
fragment_from_html(code_content)
|
|
203
343
|
else
|
|
204
344
|
Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
|
|
205
345
|
end
|
|
@@ -243,7 +383,10 @@ class Code
|
|
|
243
383
|
value_or_function.to_code
|
|
244
384
|
end
|
|
245
385
|
|
|
246
|
-
|
|
386
|
+
source = code_value.to_s
|
|
387
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
388
|
+
|
|
389
|
+
String.new(Nokogiri::HTML.fragment(source).text)
|
|
247
390
|
rescue Error::Break => e
|
|
248
391
|
e.code_value
|
|
249
392
|
end
|
|
@@ -270,14 +413,14 @@ class Code
|
|
|
270
413
|
code_contents.raw.each.with_index do |code_content, index|
|
|
271
414
|
content =
|
|
272
415
|
if code_content.is_an?(Html)
|
|
273
|
-
|
|
416
|
+
fragment_from_html(code_content)
|
|
274
417
|
else
|
|
275
418
|
Nokogiri::XML::Text.new(code_content.to_s, fragment.document)
|
|
276
419
|
end
|
|
277
420
|
|
|
278
421
|
separator =
|
|
279
422
|
if code_separator.is_an?(Html)
|
|
280
|
-
|
|
423
|
+
fragment_from_html(code_separator)
|
|
281
424
|
else
|
|
282
425
|
Nokogiri::XML::Text.new(code_separator.to_s, fragment.document)
|
|
283
426
|
end
|
|
@@ -326,14 +469,22 @@ class Code
|
|
|
326
469
|
end
|
|
327
470
|
|
|
328
471
|
if code_value.is_an?(Html)
|
|
329
|
-
Html.new(
|
|
472
|
+
Html.new(fragment_from_html(code_value))
|
|
330
473
|
else
|
|
331
|
-
|
|
474
|
+
source = code_value.to_s
|
|
475
|
+
::Code.ensure_input_size!(source, label: "html")
|
|
476
|
+
Html.new(Nokogiri::HTML::DocumentFragment.parse(source))
|
|
332
477
|
end
|
|
333
478
|
rescue Error::Break => e
|
|
334
479
|
e.code_value
|
|
335
480
|
end
|
|
336
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)
|
|
486
|
+
end
|
|
487
|
+
|
|
337
488
|
def call(**args)
|
|
338
489
|
code_operator = args.fetch(:operator, nil).to_code
|
|
339
490
|
code_arguments = args.fetch(:arguments, []).to_code
|
|
@@ -356,9 +507,15 @@ class Code
|
|
|
356
507
|
when "to_html"
|
|
357
508
|
sig(args)
|
|
358
509
|
code_to_html
|
|
510
|
+
when "inner_text"
|
|
511
|
+
sig(args)
|
|
512
|
+
code_inner_text
|
|
359
513
|
when "attribute"
|
|
360
514
|
sig(args) { String }
|
|
361
515
|
code_attribute(code_value)
|
|
516
|
+
when "attributes"
|
|
517
|
+
sig(args)
|
|
518
|
+
code_attributes
|
|
362
519
|
else
|
|
363
520
|
super
|
|
364
521
|
end
|
|
@@ -409,10 +566,25 @@ class Code
|
|
|
409
566
|
String.new(raw.text)
|
|
410
567
|
end
|
|
411
568
|
|
|
569
|
+
def code_inner_text
|
|
570
|
+
code_to_string
|
|
571
|
+
end
|
|
572
|
+
|
|
412
573
|
def code_attribute(value = nil)
|
|
413
574
|
code_value = value.to_code
|
|
414
575
|
String.new(raw.attr(code_value.to_s))
|
|
415
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
|
|
416
588
|
end
|
|
417
589
|
end
|
|
418
590
|
end
|
data/lib/code/object/http.rb
CHANGED
|
@@ -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 =
|
|
184
|
-
|
|
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
|
|
309
|
+
default_timeout = http_timeout(timeout, DEFAULT_TIMEOUT)
|
|
187
310
|
open_timeout_value =
|
|
188
|
-
open_timeout
|
|
311
|
+
http_timeout(open_timeout, default_timeout)
|
|
189
312
|
read_timeout_value =
|
|
190
|
-
read_timeout
|
|
313
|
+
http_timeout(read_timeout, default_timeout)
|
|
191
314
|
write_timeout_value =
|
|
192
|
-
write_timeout
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|