pray-cli 1.9.1 → 1.10.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.
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module Pray
7
+ module RenderDest
8
+ module_function
9
+
10
+ def materialize(project, previous_lockfile = nil)
11
+ planned = Render.planned_provisioned_files(project)
12
+ previous = previous_map(previous_lockfile)
13
+ planned_paths = planned.map(&:path).to_h { |path| [path, true] }
14
+ planned.each { |file| write_leaf(project, file, previous) }
15
+ prune_dropped(project, previous_lockfile, planned_paths) if previous_lockfile
16
+ end
17
+
18
+ def provisioned_records(project)
19
+ symbols = project.manifest.symbols || {}
20
+ Render.planned_provisioned_files(project).map do |file|
21
+ expected = Render.expected_provisioned_bytes(file.source, symbols)
22
+ ProvisionedFileRecord.new(
23
+ path: file.path.to_s.tr("\\", "/"),
24
+ content_hash: Hashing.sha256_prefixed(expected.b),
25
+ package: file.package,
26
+ export: file.export
27
+ )
28
+ end
29
+ end
30
+
31
+ def fail_if_symlink!(path, display)
32
+ return unless File.symlink?(path)
33
+
34
+ raise Error.render("refusing to write `#{display}` because it is a symbolic link")
35
+ end
36
+
37
+ def previous_map(lockfile)
38
+ return {} unless lockfile
39
+
40
+ Array(lockfile.provisioned).to_h { |record| [record.path, record] }
41
+ end
42
+
43
+ def destination_status(project, file, previous_lockfile = nil)
44
+ PathSafety.validate_destination_path!(file.path)
45
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
46
+ destination = File.join(project.project_root, file.path)
47
+ expected = Render.expected_provisioned_bytes(file.source, project.manifest.symbols || {})
48
+ record = previous_map(previous_lockfile)[file.path.to_s.tr("\\", "/")]
49
+ classify_destination(destination, file.path, expected, record)
50
+ end
51
+
52
+ def write_leaf(project, file, previous)
53
+ PathSafety.validate_destination_path!(file.path)
54
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
55
+ destination = File.join(project.project_root, file.path)
56
+ expected = Render.expected_provisioned_bytes(file.source, project.manifest.symbols || {})
57
+ record = previous[file.path.to_s.tr("\\", "/")]
58
+ status = classify_destination(destination, file.path, expected, record)
59
+ if status == :write
60
+ FileUtils.mkdir_p(File.dirname(destination))
61
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
62
+ return create_bytes(destination, file.path, expected)
63
+ end
64
+ return if status == :unchanged
65
+
66
+ unless record
67
+ raise Error.render("missing lock ownership for `#{file.path}`")
68
+ end
69
+
70
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
71
+ update_bytes(destination, file.path, expected, record.content_hash)
72
+ end
73
+
74
+ def classify_destination(destination, display, expected, record)
75
+ kind = destination_kind(destination)
76
+ return :write if kind == :missing
77
+
78
+ fail_if_symlink!(destination, display)
79
+ unless kind == :regular
80
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
81
+ end
82
+ on_disk = read_regular_bytes(destination, display)
83
+ return :unchanged if on_disk.b == expected.b
84
+ return :update if record && Hashing.sha256_prefixed(on_disk) == record.content_hash
85
+
86
+ if record
87
+ raise Error.render("refusing to overwrite `#{display}`; it was provisioned and then edited")
88
+ end
89
+
90
+ raise Error.render(
91
+ "refusing to overwrite `#{display}`; it already exists and is not the expected provisioned file"
92
+ )
93
+ end
94
+
95
+ def prune_dropped(project, previous, planned_paths)
96
+ Array(previous.provisioned).each do |record|
97
+ PathSafety.validate_destination_path!(record.path)
98
+ next if planned_paths[record.path]
99
+
100
+ destination = File.join(project.project_root, record.path)
101
+ ensure_safe_destination_ancestors!(project.project_root, record.path, record.path)
102
+ next unless destination_kind(destination) == :regular
103
+
104
+ on_disk = read_regular_bytes(destination, record.path)
105
+ if Hashing.sha256_prefixed(on_disk) == record.content_hash
106
+ ensure_safe_destination_ancestors!(project.project_root, record.path, record.path)
107
+ File.delete(destination)
108
+ end
109
+ end
110
+ end
111
+
112
+ def layout_rendered_content(path, display, fresh)
113
+ kind = destination_kind(path)
114
+ return fresh if kind == :missing
115
+
116
+ fail_if_symlink!(path, display)
117
+ unless kind == :regular
118
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
119
+ end
120
+ RenderPatch.patch_rendered_content(decode_utf8(read_regular_bytes(path, display), display), fresh)
121
+ end
122
+
123
+ def write_rendered_content(path, display, fresh)
124
+ return create_bytes(path, display, fresh) if destination_kind(path) == :missing
125
+
126
+ open_regular(path, display, File::RDWR) do |file|
127
+ existing = decode_utf8(file.read, display)
128
+ content = RenderPatch.patch_rendered_content(existing, fresh)
129
+ file.rewind
130
+ file.truncate(0)
131
+ file.write(content)
132
+ end
133
+ end
134
+
135
+ def ensure_safe_destination_ancestors!(project_root, relative_path, display)
136
+ parent = File.dirname(relative_path.to_s.tr("\\", "/"))
137
+ return if parent == "."
138
+
139
+ current = project_root
140
+ parent.split("/").each do |component|
141
+ next if component.empty? || component == "."
142
+
143
+ current = File.join(current, component)
144
+ metadata = File.lstat(current)
145
+ if metadata.symlink?
146
+ raise Error.render(
147
+ "refusing to write `#{display}` because a destination parent is a symbolic link"
148
+ )
149
+ end
150
+ unless metadata.directory?
151
+ raise Error.render(
152
+ "refusing to write `#{display}`; a destination parent is not a directory"
153
+ )
154
+ end
155
+ rescue Errno::ENOENT
156
+ next
157
+ end
158
+ end
159
+
160
+ def create_bytes(path, display, bytes)
161
+ open_path(path, display, File::WRONLY | File::CREAT | File::EXCL) do |file|
162
+ file.write(bytes)
163
+ end
164
+ end
165
+
166
+ def update_bytes(path, display, bytes, authorized_hash)
167
+ open_regular(path, display, File::RDWR) do |file|
168
+ on_disk = file.read
169
+ return if on_disk.b == bytes.b
170
+
171
+ unless Hashing.sha256_prefixed(on_disk) == authorized_hash
172
+ raise Error.render("refusing to overwrite `#{display}`; it was provisioned and then edited")
173
+ end
174
+ file.rewind
175
+ file.truncate(0)
176
+ file.write(bytes)
177
+ end
178
+ end
179
+
180
+ def read_regular_bytes(path, display)
181
+ open_regular(path, display, File::RDONLY) { |file| file.read }
182
+ end
183
+
184
+ def decode_utf8(bytes, display)
185
+ text = bytes.dup.force_encoding(Encoding::UTF_8)
186
+ return text if text.valid_encoding?
187
+
188
+ raise Error.render("rendered destination `#{display}` is not valid UTF-8")
189
+ end
190
+
191
+ def open_regular(path, display, flags)
192
+ open_path(path, display, flags) do |file|
193
+ unless file.stat.file?
194
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
195
+ end
196
+ yield file
197
+ end
198
+ end
199
+
200
+ def open_path(path, display, flags)
201
+ no_follow = File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0
202
+ File.open(path, flags | no_follow) { |file| yield file }
203
+ rescue Errno::ELOOP
204
+ raise Error.render("refusing to write `#{display}` because it is a symbolic link")
205
+ end
206
+
207
+ def destination_kind(path)
208
+ return :symlink if File.symlink?(path)
209
+ return :missing unless File.exist?(path)
210
+ return :regular if File.file?(path)
211
+
212
+ :other
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ module_function
6
+
7
+ def layout_rendered_targets(project, rendered)
8
+ rendered.map do |target|
9
+ PathSafety.validate_destination_path!(target.path)
10
+ RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
11
+ path = File.join(project.project_root, target.path)
12
+ content = RenderDest.layout_rendered_content(path, target.path, target.content)
13
+ RenderedTarget.new(
14
+ path: target.path,
15
+ content: content,
16
+ managed_spans: RenderPatch.relocate_managed_spans(content, target.managed_spans)
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module RenderPatch
5
+ module_function
6
+
7
+ def patch_rendered_content(existing, fresh)
8
+ existing_segments = split_segments(existing)
9
+ fresh_segments = split_segments(fresh)
10
+ fresh_managed = fresh_segments.filter_map do |segment|
11
+ [segment[:id], segment[:body]] if segment[:kind] == :managed
12
+ end.to_h
13
+ overlap = existing_segments.any? do |segment|
14
+ segment[:kind] == :managed && fresh_managed.key?(segment[:id])
15
+ end
16
+ return fresh unless overlap
17
+
18
+ used = {}
19
+ output = existing_segments.map do |segment|
20
+ next segment[:text] if segment[:kind] == :text
21
+
22
+ used[segment[:id]] = true
23
+ managed_segment(segment[:id], fresh_managed.fetch(segment[:id], segment[:body]))
24
+ end.join
25
+ fresh_segments.each do |segment|
26
+ next unless segment[:kind] == :managed
27
+ next if used[segment[:id]]
28
+
29
+ output << managed_segment(segment[:id], segment[:body])
30
+ end
31
+ output.end_with?("\n") ? output : "#{output}\n"
32
+ end
33
+
34
+ def relocate_managed_spans(content, spans)
35
+ positions = marker_positions(lines_of(content))
36
+ spans.map do |span|
37
+ position = positions[span.id]
38
+ next span unless position
39
+
40
+ span.dup.tap do |relocated|
41
+ relocated.open_line = position[0]
42
+ relocated.close_line = position[1]
43
+ end
44
+ end
45
+ end
46
+
47
+ def split_segments(content)
48
+ lines = lines_of(content)
49
+ segments = []
50
+ text = +""
51
+ index = 0
52
+ while index < lines.length
53
+ identifier = marker_id(lines[index])
54
+ close = find_closing_marker(lines, index + 1, identifier) if identifier
55
+ if identifier && close
56
+ segments << {kind: :text, text: text} unless text.empty?
57
+ text = +""
58
+ body_lines = lines[(index + 1)...close]
59
+ body = body_lines.empty? ? "" : "#{body_lines.join("\n")}\n"
60
+ segments << {kind: :managed, id: identifier, body: body}
61
+ index = close + 1
62
+ else
63
+ text << "#{lines[index]}\n"
64
+ index += 1
65
+ end
66
+ end
67
+ segments << {kind: :text, text: text} unless text.empty?
68
+ segments
69
+ end
70
+
71
+ def lines_of(content)
72
+ content.lines(chomp: true)
73
+ end
74
+
75
+ def find_closing_marker(lines, start, identifier)
76
+ (start...lines.length).find { |index| marker_id(lines[index]) == identifier }
77
+ end
78
+
79
+ def marker_positions(lines)
80
+ positions = {}
81
+ active = nil
82
+ lines.each_with_index do |line, index|
83
+ identifier = marker_id(line)
84
+ next unless identifier&.match?(/\A[a-z0-9]+\z/)
85
+
86
+ if active.nil?
87
+ active = [identifier, index + 1]
88
+ elsif active[0] == identifier
89
+ positions[identifier] = [active[1], index + 1]
90
+ active = nil
91
+ end
92
+ end
93
+ positions
94
+ end
95
+
96
+ def marker_id(line)
97
+ match = line.strip.match(/\A<!-- pray:(.+) -->\z/)
98
+ identifier = match && match[1]
99
+ identifier unless identifier == "0 ignore-comments"
100
+ end
101
+
102
+ def managed_segment(identifier, body)
103
+ content = body.empty? ? "" : "#{body.sub(/\n+\z/, "")}\n"
104
+ "<!-- pray:#{identifier} -->\n#{content}<!-- pray:#{identifier} -->\n"
105
+ end
106
+ end
107
+ end
data/lib/pray/resolve.rb CHANGED
@@ -87,6 +87,7 @@ module Pray
87
87
  errors << "#{declaration.name}: #{error.message}"
88
88
  end
89
89
  raise Error.resolution(errors.join("\n")) unless errors.empty?
90
+ PackageSpec.warn_resolved_deprecations(packages)
90
91
 
91
92
  local_files = []
92
93
  local_errors = []
@@ -187,16 +188,16 @@ module Pray
187
188
  return [File.expand_path(local_path, project_root), nil]
188
189
  end
189
190
  return [File.expand_path(declaration.path, project_root), nil] if declaration.path
191
+ source_name = ResolveSource.implied_source_name(declaration, sources)
192
+ if source_name
193
+ source = sources[source_name]
194
+ raise Error.resolution("unknown source: #{source_name}") unless source
190
195
 
191
- if declaration.source
192
- source = sources[declaration.source]
193
- raise Error.resolution("unknown source: #{declaration.source}") unless source
194
-
195
- if (local_path = user_config.local.source[declaration.source])
196
+ if (local_path = user_config.local.source[source_name])
196
197
  source_root = File.expand_path(local_path, project_root)
197
198
  resolved = Registry.resolve_local_registry_package_root(
198
199
  project_root,
199
- "local:#{declaration.source}",
200
+ "local:#{source_name}",
200
201
  source_root,
201
202
  declaration,
202
203
  preferred_version: lockfile_preferred_version(lockfile, declaration.name),
@@ -298,40 +299,11 @@ module Pray
298
299
  end
299
300
 
300
301
  def select_exports(declaration, spec)
301
- unless declaration.exports.empty?
302
- declaration.exports.each do |export|
303
- unless spec.exports.key?(export)
304
- raise Error.resolution("package #{declaration.name} does not export #{export}")
305
- end
306
- end
307
- return declaration.exports
308
- end
309
-
310
- roles = declaration.roles || []
311
- return spec.exports.keys.sort if roles.empty? && declaration.file.nil?
312
-
313
- effective_roles = roles.dup
314
- effective_roles << "file" if declaration.file && !effective_roles.include?("file")
302
+ ResolveExports.select_exports(declaration, spec)
303
+ end
315
304
 
316
- selected = []
317
- effective_roles.each do |role|
318
- compatible = spec.exports.filter_map do |name, export|
319
- name if Destination.export_kind_matches_role?(export.kind, role)
320
- end
321
- case compatible.length
322
- when 1
323
- selected << compatible.first unless selected.include?(compatible.first)
324
- when 0
325
- raise Error.resolution(
326
- "package #{declaration.name} has no export compatible with #{role}"
327
- )
328
- else
329
- raise Error.resolution(
330
- "package #{declaration.name} has multiple exports compatible with #{role}; set export: \"name\""
331
- )
332
- end
333
- end
334
- selected
305
+ def load_export_bodies(file_bytes, spec, selected_exports)
306
+ ResolveExports.load_export_bodies(file_bytes, spec, selected_exports)
335
307
  end
336
308
 
337
309
  def load_package_file_bytes(root, spec)
@@ -346,21 +318,6 @@ module Pray
346
318
  file_bytes
347
319
  end
348
320
 
349
- def load_export_bodies(file_bytes, spec, selected_exports)
350
- export_bodies = {}
351
- selected_exports.each do |export_name|
352
- entry = spec.exports[export_name]
353
- raise Error.resolution("package #{spec.name} is missing export #{export_name}") unless entry
354
- next unless entry.kind == "fragment"
355
-
356
- bytes = file_bytes[entry.path]
357
- raise Error.integrity("package file missing for export #{export_name}: #{entry.path}") unless bytes
358
-
359
- export_bodies[export_name] = Hashing.normalize_line_endings(bytes.force_encoding(Encoding::UTF_8))
360
- end
361
- export_bodies
362
- end
363
-
364
321
  def build_skill_file_index(spec)
365
322
  index = {}
366
323
  spec.exports.each do |export_name, export|
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResolveExports
5
+ module_function
6
+
7
+ def select_exports(declaration, spec)
8
+ unless declaration.exports.empty?
9
+ declaration.exports.each do |export|
10
+ unless spec.exports.key?(export)
11
+ raise Error.resolution("package #{declaration.name} does not export #{export}")
12
+ end
13
+ end
14
+ return declaration.exports
15
+ end
16
+
17
+ roles = declaration.roles || []
18
+ return spec.exports.keys.sort if roles.empty? && declaration.file.nil?
19
+
20
+ effective_roles = roles.dup
21
+ effective_roles << "file" if declaration.file && !effective_roles.include?("file")
22
+
23
+ selected = []
24
+ effective_roles.each do |role|
25
+ compatible = if role == "fragment"
26
+ fragment_role_exports(spec)
27
+ else
28
+ spec.exports.filter_map do |name, export|
29
+ name if Destination.export_kind_matches_role?(export.kind, role)
30
+ end
31
+ end
32
+ case compatible.length
33
+ when 1
34
+ selected << compatible.first unless selected.include?(compatible.first)
35
+ when 0
36
+ raise Error.resolution(
37
+ "package #{declaration.name} has no export compatible with #{role}"
38
+ )
39
+ else
40
+ raise Error.resolution(
41
+ "package #{declaration.name} has multiple exports compatible with #{role}; set export: \"name\""
42
+ )
43
+ end
44
+ end
45
+ selected
46
+ end
47
+
48
+ def load_export_bodies(file_bytes, spec, selected_exports)
49
+ export_bodies = {}
50
+ selected_exports.each do |export_name|
51
+ entry = spec.exports[export_name]
52
+ raise Error.resolution("package #{spec.name} is missing export #{export_name}") unless entry
53
+ next unless %w[fragment file].include?(entry.kind)
54
+
55
+ bytes = file_bytes[entry.path]
56
+ unless bytes
57
+ raise Error.integrity("package file missing for export #{export_name}: #{entry.path}")
58
+ end
59
+
60
+ text = bytes.dup.force_encoding(Encoding::UTF_8)
61
+ unless text.valid_encoding?
62
+ if entry.kind == "fragment"
63
+ raise Error.integrity("package file is not valid utf-8 for export #{export_name}")
64
+ end
65
+ next
66
+ end
67
+ export_bodies[export_name] = Hashing.normalize_line_endings(text)
68
+ end
69
+ export_bodies
70
+ end
71
+
72
+ def fragment_role_exports(spec)
73
+ fragments = spec.exports.filter_map { |name, export| name if export.kind == "fragment" }
74
+ return fragments unless fragments.empty?
75
+
76
+ spec.exports.filter_map { |name, export| name if export.kind == "file" }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResolveSource
5
+ module_function
6
+
7
+ def implied_source_name(declaration, sources)
8
+ return declaration.source if declaration.source
9
+
10
+ namespace = package_namespace(declaration.name)
11
+ return namespace if namespace && sources.key?(namespace)
12
+
13
+ case sources.length
14
+ when 0 then nil
15
+ when 1 then sources.keys.first
16
+ else
17
+ raise Error.resolution(
18
+ "package #{declaration.name} requires source: when multiple sources are declared " \
19
+ "and the package namespace does not match a source"
20
+ )
21
+ end
22
+ end
23
+
24
+ def package_namespace(name)
25
+ name.include?("/") ? name.split("/", 2).first : nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module ResourceLimits
5
+ MAX_ARCHIVE_ENTRIES = 10_000
6
+ MAX_ARCHIVE_ENTRY_BYTES = 32 * 1024 * 1024
7
+ MAX_ARCHIVE_TOTAL_BYTES = 64 * 1024 * 1024
8
+ MAX_HTTP_RESPONSE_BYTES = 64 * 1024 * 1024
9
+ MAX_SERVE_BODY_BYTES = 16 * 1024 * 1024
10
+ MAX_SERVE_HEADER_BYTES = 64 * 1024
11
+ MAX_SERVE_CONCURRENT_CONNECTIONS = 32
12
+ SERVE_SOCKET_TIMEOUT_SECONDS = 30
13
+ end
14
+ end
data/lib/pray/serve.rb CHANGED
@@ -4,12 +4,14 @@ require "socket"
4
4
  require "fileutils"
5
5
  require "pathname"
6
6
  require "json"
7
+ require "timeout"
7
8
  require_relative "path_safety"
9
+ require_relative "resource_limits"
8
10
  require_relative "serve_federation"
9
11
 
10
12
  module Pray
11
13
  module Serve
12
- DEFAULT_MAX_CONNECTIONS = 8
14
+ DEFAULT_MAX_CONNECTIONS = ResourceLimits::MAX_SERVE_CONCURRENT_CONNECTIONS
13
15
 
14
16
  module_function
15
17
 
@@ -39,7 +41,14 @@ module Pray
39
41
  end
40
42
 
41
43
  def serve_connection(root, socket, connection_slots)
42
- handle_connection(root, socket)
44
+ Timeout.timeout(ResourceLimits::SERVE_SOCKET_TIMEOUT_SECONDS) do
45
+ handle_connection(root, socket)
46
+ end
47
+ rescue Timeout::Error
48
+ socket.print(request_timeout) unless socket.closed?
49
+ rescue Error => error
50
+ response = error.message.include?("request body exceeds") ? payload_too_large : bad_request
51
+ socket.print(response) unless socket.closed?
43
52
  ensure
44
53
  connection_slots << true unless connection_slots.closed?
45
54
  socket.close unless socket.closed?
@@ -51,7 +60,9 @@ module Pray
51
60
 
52
61
  method, path, = request_line.split
53
62
  headers = read_headers(socket)
54
- body_length = headers["content-length"].to_i
63
+ body_length = Integer(headers.fetch("content-length", "0"), exception: false)
64
+ raise Error.parse("request", "invalid content length") unless body_length
65
+ validate_body_length!(body_length)
55
66
  body = body_length.positive? ? socket.read(body_length) : ""
56
67
 
57
68
  response = dispatch_request(root, method, path, body)
@@ -60,9 +71,14 @@ module Pray
60
71
 
61
72
  def read_headers(socket)
62
73
  headers = {}
74
+ header_bytes = 0
63
75
  loop do
64
76
  line = socket.gets
65
77
  break if line.nil? || line.strip.empty?
78
+ header_bytes += line.bytesize
79
+ if header_bytes > ResourceLimits::MAX_SERVE_HEADER_BYTES
80
+ raise Error.parse("request", "request headers exceed server limit")
81
+ end
66
82
 
67
83
  name, value = line.split(":", 2)
68
84
  headers[name.strip.downcase] = value.strip if name && value
@@ -70,10 +86,20 @@ module Pray
70
86
  headers
71
87
  end
72
88
 
89
+ def validate_body_length!(body_length)
90
+ return if body_length <= ResourceLimits::MAX_SERVE_BODY_BYTES
91
+
92
+ raise Error.unsupported(
93
+ "request body exceeds #{ResourceLimits::MAX_SERVE_BODY_BYTES} bytes"
94
+ )
95
+ end
96
+
73
97
  def dispatch_request(root, method, path, body = "")
74
98
  path = path.split("?", 2).first
75
99
 
76
100
  case [method, path]
101
+ when ["GET", "/health"]
102
+ return ok_response("text/plain", "ok")
77
103
  when ["GET", "/.well-known/pray-federation.json"]
78
104
  return ServeFederation.discovery_response(root)
79
105
  when ["GET", "/v1/sync/index"]
@@ -89,12 +115,13 @@ module Pray
89
115
  return not_found unless method == "GET"
90
116
 
91
117
  if path == "/"
92
- return html_response("<h1>Pray distribution</h1><p>Root: #{root}</p>")
118
+ return html_response("<h1>Pray distribution</h1>")
93
119
  end
94
120
 
95
121
  file_path = PathSafety.join_under_root(root, path.delete_prefix("/"))
96
122
  return not_found unless file_path
97
123
  return not_found unless File.file?(file_path)
124
+ return payload_too_large if File.size(file_path) > ResourceLimits::MAX_HTTP_RESPONSE_BYTES
98
125
 
99
126
  content_type = content_type_for(file_path)
100
127
  file_body = File.binread(file_path)
@@ -126,5 +153,20 @@ module Pray
126
153
  body = "too many connections"
127
154
  "HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
128
155
  end
156
+
157
+ def request_timeout
158
+ body = "request timed out"
159
+ "HTTP/1.1 408 Request Timeout\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
160
+ end
161
+
162
+ def payload_too_large
163
+ body = "request exceeds server limit"
164
+ "HTTP/1.1 413 Payload Too Large\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
165
+ end
166
+
167
+ def bad_request
168
+ body = "bad request"
169
+ "HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
170
+ end
129
171
  end
130
172
  end