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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/bin/code +100 -20
- data/lib/code/concerns/shared.rb +335 -15
- data/lib/code/format.rb +33 -15
- data/lib/code/network.rb +82 -0
- data/lib/code/node/call.rb +80 -2
- data/lib/code/node/call_argument.rb +14 -0
- data/lib/code/node/code.rb +4 -3
- data/lib/code/node/function_parameter.rb +7 -4
- data/lib/code/node/list.rb +32 -2
- data/lib/code/node/square_bracket.rb +4 -2
- data/lib/code/object/base_64.rb +132 -6
- data/lib/code/object/boolean.rb +56 -0
- data/lib/code/object/class.rb +143 -2
- data/lib/code/object/code.rb +108 -7
- data/lib/code/object/context.rb +59 -1
- data/lib/code/object/cryptography.rb +69 -0
- data/lib/code/object/date.rb +13800 -462
- data/lib/code/object/decimal.rb +1098 -0
- data/lib/code/object/dictionary.rb +1861 -11
- data/lib/code/object/duration.rb +24 -0
- data/lib/code/object/function.rb +289 -27
- data/lib/code/object/global.rb +447 -1
- data/lib/code/object/html.rb +181 -7
- data/lib/code/object/http.rb +253 -17
- data/lib/code/object/ics.rb +76 -13
- data/lib/code/object/identifier_list.rb +30 -10
- data/lib/code/object/integer.rb +1265 -2
- data/lib/code/object/json.rb +80 -1
- data/lib/code/object/list.rb +3371 -10
- data/lib/code/object/nothing.rb +53 -0
- data/lib/code/object/number.rb +120 -0
- data/lib/code/object/parameter.rb +149 -0
- data/lib/code/object/range.rb +530 -14
- data/lib/code/object/smtp.rb +103 -12
- data/lib/code/object/string.rb +968 -3
- data/lib/code/object/super.rb +11 -1
- data/lib/code/object/time.rb +13932 -498
- data/lib/code/object/url.rb +67 -0
- data/lib/code/object.rb +582 -0
- data/lib/code/parser.rb +194 -55
- 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,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(\"<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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
474
|
+
Html.new(fragment_from_html(code_value))
|
|
330
475
|
else
|
|
331
|
-
|
|
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
|
data/lib/code/object/http.rb
CHANGED
|
@@ -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 =
|
|
184
|
-
|
|
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
|
|
187
|
-
open_timeout_value =
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|