capsium 0.1.1 → 0.2.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/.github/workflows/rake.yml +1 -4
- data/.github/workflows/release.yml +1 -2
- data/.gitignore +1 -0
- data/.rubocop.yml +34 -3
- data/CHANGELOG.md +58 -0
- data/Gemfile +4 -4
- data/LICENSE +21 -0
- data/README.adoc +177 -135
- data/Rakefile +8 -1
- data/bin/console +1 -1
- data/capsium.gemspec +16 -23
- data/exe/capsium +1 -1
- data/lib/capsium/cli/convert.rb +18 -0
- data/lib/capsium/cli/package.rb +86 -0
- data/lib/capsium/cli/reactor.rb +25 -0
- data/lib/capsium/cli.rb +5 -79
- data/lib/capsium/converters/jekyll.rb +62 -31
- data/lib/capsium/converters.rb +7 -0
- data/lib/capsium/package/dataset.rb +76 -56
- data/lib/capsium/package/manifest.rb +41 -50
- data/lib/capsium/package/manifest_config.rb +64 -0
- data/lib/capsium/package/metadata.rb +11 -29
- data/lib/capsium/package/metadata_config.rb +105 -0
- data/lib/capsium/package/routes.rb +51 -111
- data/lib/capsium/package/routes_config.rb +150 -0
- data/lib/capsium/package/security.rb +123 -0
- data/lib/capsium/package/security_config.rb +60 -0
- data/lib/capsium/package/storage.rb +26 -26
- data/lib/capsium/package/storage_config.rb +106 -0
- data/lib/capsium/package/validator.rb +149 -0
- data/lib/capsium/package.rb +84 -70
- data/lib/capsium/packager.rb +60 -62
- data/lib/capsium/reactor/introspection.rb +88 -0
- data/lib/capsium/reactor.rb +108 -36
- data/lib/capsium/version.rb +1 -1
- data/lib/capsium.rb +7 -9
- data/sig/capsium/package/dataset.rbs +28 -0
- data/sig/capsium/package/manifest.rbs +34 -0
- data/sig/capsium/package/manifest_config.rbs +38 -0
- data/sig/capsium/package/metadata.rbs +26 -0
- data/sig/capsium/package/metadata_config.rbs +49 -0
- data/sig/capsium/package/routes.rbs +37 -0
- data/sig/capsium/package/routes_config.rbs +74 -0
- data/sig/capsium/package/security.rbs +65 -0
- data/sig/capsium/package/security_config.rbs +57 -0
- data/sig/capsium/package/storage.rbs +28 -0
- data/sig/capsium/package/storage_config.rbs +61 -0
- data/sig/capsium/package/validator.rbs +29 -0
- data/sig/capsium/package.rbs +53 -0
- data/sig/capsium/packager.rbs +23 -0
- data/sig/capsium/reactor/introspection.rbs +31 -0
- data/sig/capsium/reactor.rbs +30 -0
- data/sig/capsium.rbs +3 -1
- metadata +63 -146
- data/images/005df7415a331c466ad2d9a42efdd212b6f42fe50b5fd3b3174c86c706f58244.png +0 -0
- data/images/0374025b3af99b8a473282c8cbbf9fcd29573cf41e586982f328f86c0690f43d.png +0 -0
- data/images/0bb4da785be40ef58e219470ebccb979325928b75453dc46bac23c6ee8a7a7cb.png +0 -0
- data/images/6aa294dccc81af594aacbe804e7ddffdc17eacc28357338108aea5d021d831ff.png +0 -0
- data/images/72dd3fbf3f4b475e27a0e7fb8137c475c32c41f8d222bcf62d6a9ccf102d9532.png +0 -0
- data/images/8772b6961d169738d7b0fa0b669b06fc2f40632d4c62586c7634fc17b93182a3.png +0 -0
- data/images/a998d842405933d45723606ff3f70162ec95b4ef30db25464a366184fd08fb9b.png +0 -0
- data/images/aa8980547e8c003d33273ab4d80e62da7f317bd7581b293c06d67f5331f24f31.png +0 -0
- data/images/bb78a872b539e0e9b2d80dee58acbb688f3f2727b324a5bf8bf417a69d94a166.png +0 -0
- data/images/c48fc83b17725d85fbb64d971196ebfccd8c5c757fe6aa5845303f6e315879b6.png +0 -0
- data/images/f08ef07308d08119ac2124bb7428c8bef17ef1ca70045696604d6e83015a9b91.png +0 -0
- data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726-1.png +0 -0
- data/images/f7514206111b695647eae9adfcf498ba3e0ff83ecfe25f3fc3ed8e9f04c5c726.png +0 -0
- data/lib/capsium/protector.rb +0 -95
data/lib/capsium/package.rb
CHANGED
|
@@ -1,39 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/capsium/package.rb
|
|
4
3
|
require "fileutils"
|
|
5
|
-
require "
|
|
6
|
-
require "
|
|
7
|
-
require "csv"
|
|
8
|
-
require "sqlite3"
|
|
9
|
-
require "zip"
|
|
10
|
-
require_relative "package/manifest"
|
|
11
|
-
require_relative "package/metadata"
|
|
12
|
-
require_relative "package/routes"
|
|
13
|
-
require_relative "package/dataset"
|
|
14
|
-
require_relative "package/storage"
|
|
15
|
-
require_relative "packager"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "tmpdir"
|
|
16
6
|
|
|
17
7
|
module Capsium
|
|
18
8
|
class Package
|
|
19
|
-
|
|
9
|
+
autoload :Dataset, "capsium/package/dataset"
|
|
10
|
+
autoload :DatasetConfig, "capsium/package/storage_config"
|
|
11
|
+
autoload :DigitalSignatures, "capsium/package/security_config"
|
|
12
|
+
autoload :IntegrityChecks, "capsium/package/security_config"
|
|
13
|
+
autoload :Manifest, "capsium/package/manifest"
|
|
14
|
+
autoload :ManifestConfig, "capsium/package/manifest_config"
|
|
15
|
+
autoload :Metadata, "capsium/package/metadata"
|
|
16
|
+
autoload :MetadataData, "capsium/package/metadata_config"
|
|
17
|
+
autoload :Repository, "capsium/package/metadata_config"
|
|
18
|
+
autoload :Resource, "capsium/package/manifest_config"
|
|
19
|
+
autoload :Route, "capsium/package/routes_config"
|
|
20
|
+
autoload :Routes, "capsium/package/routes"
|
|
21
|
+
autoload :RoutesConfig, "capsium/package/routes_config"
|
|
22
|
+
autoload :Security, "capsium/package/security"
|
|
23
|
+
autoload :SecurityConfig, "capsium/package/security_config"
|
|
24
|
+
autoload :SecurityData, "capsium/package/security_config"
|
|
25
|
+
autoload :Storage, "capsium/package/storage"
|
|
26
|
+
autoload :StorageConfig, "capsium/package/storage_config"
|
|
27
|
+
autoload :StorageData, "capsium/package/storage_config"
|
|
28
|
+
autoload :Validator, "capsium/package/validator"
|
|
29
|
+
|
|
30
|
+
attr_reader :name, :path, :manifest, :metadata, :routes, :storage,
|
|
31
|
+
:security, :load_type
|
|
20
32
|
|
|
21
33
|
MANIFEST_FILE = "manifest.json"
|
|
22
34
|
METADATA_FILE = "metadata.json"
|
|
23
|
-
PACKAGING_FILE = "packaging.json"
|
|
24
|
-
SIGNATURE_FILE = "signature.json"
|
|
25
35
|
STORAGE_FILE = "storage.json"
|
|
26
36
|
ROUTES_FILE = "routes.json"
|
|
37
|
+
SECURITY_FILE = "security.json"
|
|
27
38
|
CONTENT_DIR = "content"
|
|
28
39
|
DATA_DIR = "data"
|
|
29
|
-
ENCRYPTED_PACKAGING_FILE = "package.enc"
|
|
30
40
|
|
|
31
|
-
def initialize(path)
|
|
41
|
+
def initialize(path, load_type: nil)
|
|
32
42
|
@original_path = Pathname.new(path).expand_path
|
|
33
|
-
@path = prepare_package(@original_path)
|
|
43
|
+
@path = prepare_package(@original_path).to_s
|
|
44
|
+
@load_type = load_type || determine_load_type(path)
|
|
34
45
|
create_package_structure
|
|
35
46
|
load_package
|
|
36
47
|
@name = metadata.name
|
|
48
|
+
verify_integrity!
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
def prepare_package(path)
|
|
@@ -42,47 +54,23 @@ module Capsium
|
|
|
42
54
|
if File.file?(path)
|
|
43
55
|
return decompress_cap_file(path) if File.extname(path) == ".cap"
|
|
44
56
|
|
|
45
|
-
raise
|
|
46
|
-
|
|
57
|
+
raise Error, "The package must have a .cap extension"
|
|
47
58
|
end
|
|
48
59
|
|
|
49
|
-
raise "Invalid package path: #{path}"
|
|
60
|
+
raise Error, "Invalid package path: #{path}"
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
def solidify
|
|
53
64
|
@manifest.save_to_file
|
|
54
65
|
@metadata.save_to_file
|
|
55
66
|
@routes.save_to_file
|
|
56
|
-
@storage.save_to_file
|
|
67
|
+
@storage.save_to_file unless @storage.empty?
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
def decompress_cap_file(file_path)
|
|
60
|
-
|
|
61
|
-
metadata_path = File.join(temp_dir, METADATA_FILE)
|
|
62
|
-
|
|
63
|
-
# Extract metadata.json first
|
|
64
|
-
Zip::File.open(file_path) do |zip_file|
|
|
65
|
-
if entry = zip_file.find_entry(METADATA_FILE)
|
|
66
|
-
entry.extract(metadata_path)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
metadata = Metadata.new(metadata_path)
|
|
71
|
-
package_name = metadata.name
|
|
72
|
-
package_version = metadata.version
|
|
73
|
-
package_dependencies = metadata.dependencies
|
|
74
|
-
|
|
75
|
-
package_path = File.join(temp_dir, "#{package_name}-#{package_version}")
|
|
71
|
+
package_path = File.join(Dir.mktmpdir, package_stem(file_path))
|
|
76
72
|
FileUtils.mkdir_p(package_path)
|
|
77
|
-
|
|
78
|
-
Zip::File.open(file_path) do |zip_file|
|
|
79
|
-
zip_file.each do |entry|
|
|
80
|
-
entry_path = File.join(package_path, entry.name)
|
|
81
|
-
FileUtils.mkdir_p(File.dirname(entry_path))
|
|
82
|
-
entry.extract(entry_path)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
73
|
+
Packager.new.unpack(file_path, package_path)
|
|
86
74
|
package_path
|
|
87
75
|
end
|
|
88
76
|
|
|
@@ -92,51 +80,77 @@ module Capsium
|
|
|
92
80
|
|
|
93
81
|
# Optional
|
|
94
82
|
@manifest = Manifest.new(manifest_path)
|
|
95
|
-
@routes = Routes.new(routes_path, @manifest)
|
|
96
83
|
@storage = Storage.new(storage_path)
|
|
97
|
-
|
|
84
|
+
@routes = Routes.new(routes_path, @manifest, @storage)
|
|
85
|
+
@security = Security.new(security_path)
|
|
98
86
|
end
|
|
99
87
|
|
|
100
88
|
def cleanup
|
|
101
|
-
return unless @path != @original_path && File.directory?(@path)
|
|
89
|
+
return unless @path != @original_path.to_s && File.directory?(@path)
|
|
102
90
|
|
|
103
91
|
FileUtils.remove_entry(@path)
|
|
104
92
|
end
|
|
105
93
|
|
|
106
|
-
def
|
|
107
|
-
|
|
94
|
+
def datasets
|
|
95
|
+
storage.datasets
|
|
108
96
|
end
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
FileUtils.mkdir_p(content_path)
|
|
115
|
-
FileUtils.mkdir_p(data_path)
|
|
98
|
+
# The .cap file this package was loaded from, or nil when loaded
|
|
99
|
+
# from a directory.
|
|
100
|
+
def cap_file_path
|
|
101
|
+
@original_path.to_s if load_type == :cap_file
|
|
116
102
|
end
|
|
117
103
|
|
|
118
|
-
def
|
|
119
|
-
File.join(
|
|
104
|
+
def content_files
|
|
105
|
+
Dir.glob(File.join(content_path, "**", "*")).select do |file|
|
|
106
|
+
File.file?(file)
|
|
107
|
+
end
|
|
120
108
|
end
|
|
121
109
|
|
|
122
|
-
def
|
|
123
|
-
File.
|
|
110
|
+
def determine_load_type(path)
|
|
111
|
+
return :directory if File.directory?(path)
|
|
112
|
+
return :cap_file if File.extname(path) == ".cap"
|
|
113
|
+
|
|
114
|
+
:unsaved
|
|
124
115
|
end
|
|
125
116
|
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
# Verifies the package against security.json (ARCHITECTURE.md section
|
|
118
|
+
# 6). Returns a list of typed errors; empty when no security.json is
|
|
119
|
+
# present or all checksums match.
|
|
120
|
+
def verify_integrity
|
|
121
|
+
return [] unless @security.present?
|
|
122
|
+
|
|
123
|
+
@security.verify(@path)
|
|
128
124
|
end
|
|
129
125
|
|
|
130
|
-
def
|
|
131
|
-
|
|
126
|
+
def verify_integrity!
|
|
127
|
+
@security.verify!(@path) if @security.present?
|
|
132
128
|
end
|
|
133
129
|
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def package_stem(file_path)
|
|
133
|
+
File.basename(file_path, ".cap")
|
|
136
134
|
end
|
|
137
135
|
|
|
138
|
-
def
|
|
139
|
-
|
|
136
|
+
def create_package_structure
|
|
137
|
+
FileUtils.mkdir_p(@path)
|
|
138
|
+
FileUtils.mkdir_p(content_path)
|
|
139
|
+
FileUtils.mkdir_p(data_path)
|
|
140
140
|
end
|
|
141
|
+
|
|
142
|
+
def content_path = File.join(@path, CONTENT_DIR)
|
|
143
|
+
|
|
144
|
+
def data_path = File.join(@path, DATA_DIR)
|
|
145
|
+
|
|
146
|
+
def routes_path = File.join(@path, ROUTES_FILE)
|
|
147
|
+
|
|
148
|
+
def storage_path = File.join(@path, STORAGE_FILE)
|
|
149
|
+
|
|
150
|
+
def metadata_path = File.join(@path, METADATA_FILE)
|
|
151
|
+
|
|
152
|
+
def manifest_path = File.join(@path, MANIFEST_FILE)
|
|
153
|
+
|
|
154
|
+
def security_path = File.join(@path, SECURITY_FILE)
|
|
141
155
|
end
|
|
142
156
|
end
|
data/lib/capsium/packager.rb
CHANGED
|
@@ -1,100 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/capsium/packager.rb
|
|
4
|
-
require "json"
|
|
5
3
|
require "fileutils"
|
|
6
|
-
require "
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "tmpdir"
|
|
7
6
|
require "zip"
|
|
8
7
|
|
|
9
8
|
module Capsium
|
|
10
9
|
class Packager
|
|
11
10
|
class FileAlreadyExistsError < StandardError; end
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
12
|
+
# Raised when a .cap archive contains an entry whose name would be
|
|
13
|
+
# written outside the destination directory (absolute paths, drive
|
|
14
|
+
# letters, ".." segments) — zip-slip protection, on par with the Lua
|
|
15
|
+
# reactor's extractor.
|
|
16
|
+
class UnsafeEntryError < Capsium::Error; end
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
unless options[:force]
|
|
22
|
-
raise FileAlreadyExistsError,
|
|
23
|
-
"Package target already exists, aborting: `#{relative_path_current(cap_file_path)}`"
|
|
24
|
-
end
|
|
18
|
+
DRIVE_LETTER_PATTERN = %r{\A[A-Za-z]:[/\\]}
|
|
25
19
|
|
|
20
|
+
def pack(package, options = {})
|
|
21
|
+
directory = package.path
|
|
22
|
+
output_file_name = "#{package.metadata.name}-#{package.metadata.version}.cap"
|
|
23
|
+
output_directory = File.dirname(directory)
|
|
24
|
+
cap_file_path = File.join(output_directory, output_file_name)
|
|
25
|
+
|
|
26
|
+
if File.exist?(cap_file_path) && !options[:force]
|
|
27
|
+
raise FileAlreadyExistsError,
|
|
28
|
+
"Package target already exists, aborting: `#{relative_path_current(cap_file_path)}`"
|
|
29
|
+
elsif File.exist?(cap_file_path)
|
|
26
30
|
puts "Package target already exists, overwriting: `#{relative_path_current(cap_file_path)}`"
|
|
27
31
|
FileUtils.rm_f(cap_file_path)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
Dir.mktmpdir do |dir|
|
|
31
|
-
FileUtils.cp_r("#{
|
|
35
|
+
FileUtils.cp_r("#{directory}/.", dir)
|
|
36
|
+
FileUtils.rm_f(File.join(dir, Package::SECURITY_FILE))
|
|
32
37
|
new_package = Package.new(dir)
|
|
33
38
|
new_package.solidify
|
|
34
|
-
|
|
39
|
+
generate_security(new_package)
|
|
40
|
+
new_cap_file_path = File.join(dir, output_file_name)
|
|
41
|
+
compress_package(new_package, new_cap_file_path)
|
|
42
|
+
puts "Package built at: #{new_cap_file_path}"
|
|
43
|
+
FileUtils.mv(new_cap_file_path, cap_file_path)
|
|
35
44
|
puts "Package created: #{relative_path_current(cap_file_path)}"
|
|
45
|
+
return cap_file_path
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unpack(cap_file_path, destination)
|
|
50
|
+
destination = File.expand_path(destination)
|
|
51
|
+
Zip::File.open(cap_file_path) do |zip_file|
|
|
52
|
+
zip_file.each do |entry|
|
|
53
|
+
entry_path = safe_entry_path(destination, entry.name)
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(entry_path))
|
|
55
|
+
entry.extract(entry_path)
|
|
56
|
+
end
|
|
36
57
|
end
|
|
37
58
|
end
|
|
38
59
|
|
|
39
60
|
def compress_package(package, cap_file)
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
entries = Dir[File.join(package.path, "**", "**")].reject do |file|
|
|
62
|
+
File.expand_path(file) == File.expand_path(cap_file)
|
|
63
|
+
end
|
|
64
|
+
Zip::File.open(cap_file, create: true) do |zipfile|
|
|
65
|
+
entries.each do |file|
|
|
42
66
|
zipfile.add(file.sub("#{package.path}/", ""), file)
|
|
43
67
|
end
|
|
44
68
|
end
|
|
45
69
|
end
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# create_packaging_file
|
|
51
|
-
|
|
52
|
-
# compressor = Compressor.new(@package, @package.metadata[:compression])
|
|
53
|
-
# compressor.compress
|
|
54
|
-
|
|
55
|
-
# protector = Protector.new(@package, @package.metadata[:encryption], @package.metadata[:signature])
|
|
56
|
-
# protector.apply_encryption_and_sign
|
|
57
|
-
# end
|
|
58
|
-
|
|
59
|
-
# private
|
|
71
|
+
def relative_path_current(absolute_path)
|
|
72
|
+
Pathname.new(absolute_path).relative_path_from(Dir.pwd).to_s
|
|
73
|
+
end
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
# metadata_path = File.join(@package.path, Package::METADATA_FILE)
|
|
63
|
-
# metadata_content = @package.metadata.to_h
|
|
64
|
-
# write_json_file(metadata_path, metadata_content)
|
|
65
|
-
# end
|
|
75
|
+
private
|
|
66
76
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
# Resolves an entry name against the destination and returns the
|
|
78
|
+
# absolute target path, raising UnsafeEntryError when the entry would
|
|
79
|
+
# escape the destination (zip-slip).
|
|
80
|
+
def safe_entry_path(destination, entry_name)
|
|
81
|
+
entry_path = File.expand_path(entry_name, destination)
|
|
82
|
+
return entry_path if contained?(entry_path, destination) &&
|
|
83
|
+
!entry_name.match?(DRIVE_LETTER_PATTERN)
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
packaging_content = {
|
|
78
|
-
name: @package.name,
|
|
79
|
-
content_path: relative_path_package(@package.content_path),
|
|
80
|
-
data_path: relative_path_package(@package.data_path),
|
|
81
|
-
datasets: @package.datasets.map(&:to_h)
|
|
82
|
-
}
|
|
83
|
-
write_json_file(packaging_path, packaging_content)
|
|
85
|
+
raise UnsafeEntryError,
|
|
86
|
+
"Refusing to extract unsafe zip entry: #{entry_name}"
|
|
84
87
|
end
|
|
85
88
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
file.write(JSON.pretty_generate(content))
|
|
89
|
-
end
|
|
89
|
+
def contained?(entry_path, destination)
|
|
90
|
+
entry_path.start_with?("#{destination}#{File::SEPARATOR}")
|
|
90
91
|
end
|
|
91
92
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def relative_path_current(absolute_path)
|
|
97
|
-
Pathname.new(absolute_path).relative_path_from(Dir.pwd).to_s
|
|
93
|
+
def generate_security(package)
|
|
94
|
+
security = Package::Security.generate(package.path)
|
|
95
|
+
security.save_to_file
|
|
98
96
|
end
|
|
99
97
|
end
|
|
100
98
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Capsium
|
|
8
|
+
class Reactor
|
|
9
|
+
# Monitoring HTTP API reports for the package this reactor serves
|
|
10
|
+
# (ARCHITECTURE.md section 7). Each report wraps its single entry in
|
|
11
|
+
# the list shape all reactors converge on.
|
|
12
|
+
class Introspection
|
|
13
|
+
METADATA_PATH = "/api/v1/introspect/metadata"
|
|
14
|
+
ROUTES_PATH = "/api/v1/introspect/routes"
|
|
15
|
+
CONTENT_HASHES_PATH = "/api/v1/introspect/content-hashes"
|
|
16
|
+
CONTENT_VALIDITY_PATH = "/api/v1/introspect/content-validity"
|
|
17
|
+
PATHS = [METADATA_PATH, ROUTES_PATH, CONTENT_HASHES_PATH,
|
|
18
|
+
CONTENT_VALIDITY_PATH].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :package
|
|
21
|
+
|
|
22
|
+
def initialize(package)
|
|
23
|
+
@package = package
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def endpoint?(path)
|
|
27
|
+
PATHS.include?(path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The report body for an introspection endpoint, or nil when the
|
|
31
|
+
# path is not an introspection endpoint.
|
|
32
|
+
def report_for(path)
|
|
33
|
+
case path
|
|
34
|
+
when METADATA_PATH then metadata_report
|
|
35
|
+
when ROUTES_PATH then routes_report
|
|
36
|
+
when CONTENT_HASHES_PATH then content_hashes_report
|
|
37
|
+
when CONTENT_VALIDITY_PATH then content_validity_report
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def metadata_report
|
|
42
|
+
metadata = package.metadata
|
|
43
|
+
{ packages: [{
|
|
44
|
+
name: metadata.name,
|
|
45
|
+
version: metadata.version,
|
|
46
|
+
author: metadata.author,
|
|
47
|
+
description: metadata.description
|
|
48
|
+
}] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def routes_report
|
|
52
|
+
entries = package.routes.config.routes.map do |route|
|
|
53
|
+
{ method: route.http_method || "GET", path: route.path }
|
|
54
|
+
end
|
|
55
|
+
{ routes: [{ package: package.name, routes: entries }] }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def content_hashes_report
|
|
59
|
+
{ contentHashes: [{ package: package.name, hash: content_hash }] }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def content_validity_report
|
|
63
|
+
errors = package.verify_integrity
|
|
64
|
+
entry = {
|
|
65
|
+
package: package.name,
|
|
66
|
+
valid: errors.empty?,
|
|
67
|
+
lastChecked: Time.now.utc.iso8601
|
|
68
|
+
}
|
|
69
|
+
entry[:reason] = errors.map(&:message).join("; ") unless errors.empty?
|
|
70
|
+
{ contentValidity: [entry] }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# SHA-256 of the .cap blob when the package was loaded from one.
|
|
76
|
+
# For directory sources there is no blob, so the hash covers a
|
|
77
|
+
# canonical (sorted-key) JSON serialization of the package content
|
|
78
|
+
# checksums — the same data security.json integrityChecks carry.
|
|
79
|
+
def content_hash
|
|
80
|
+
cap_file = package.cap_file_path
|
|
81
|
+
return Digest::SHA256.file(cap_file).hexdigest if cap_file
|
|
82
|
+
|
|
83
|
+
checksums = Package::Security.checksums_for(package.path)
|
|
84
|
+
Digest::SHA256.hexdigest(JSON.generate(checksums))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/capsium/reactor.rb
CHANGED
|
@@ -1,69 +1,78 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/capsium/reactor.rb
|
|
4
|
-
require "webrick"
|
|
5
3
|
require "json"
|
|
6
4
|
require "listen"
|
|
7
|
-
require "
|
|
5
|
+
require "webrick"
|
|
8
6
|
|
|
9
7
|
module Capsium
|
|
10
8
|
class Reactor
|
|
9
|
+
autoload :Introspection, "capsium/reactor/introspection"
|
|
10
|
+
|
|
11
11
|
DEFAULT_PORT = 8864
|
|
12
|
-
|
|
12
|
+
DEFAULT_CACHE_CONTROL = "public, max-age=31536000"
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
attr_reader :package, :package_path, :routes, :port, :cache_control,
|
|
15
|
+
:server, :server_thread, :introspection
|
|
16
|
+
|
|
17
|
+
def initialize(package:, port: DEFAULT_PORT,
|
|
18
|
+
cache_control: DEFAULT_CACHE_CONTROL, do_not_listen: false)
|
|
19
|
+
@package = package.is_a?(String) ? Package.new(package) : package
|
|
20
|
+
@package_path = @package.path
|
|
17
21
|
@port = port
|
|
18
|
-
@
|
|
19
|
-
|
|
22
|
+
@cache_control = cache_control
|
|
23
|
+
setup_server(do_not_listen)
|
|
24
|
+
load_state
|
|
20
25
|
mount_routes
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def serve
|
|
29
|
+
trap("INT") { shutdown_server }
|
|
24
30
|
@server_thread = start_server
|
|
25
31
|
start_listener
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
def handle_request(request, response)
|
|
35
|
+
return serve_introspection(request, response) if @introspection.endpoint?(request.path)
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
route = @routes.resolve(request.path)
|
|
38
|
+
route ? serve_route(route, response) : respond_not_found(response)
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
def mount_routes
|
|
35
|
-
@routes.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
content_path = target.fs_path(@package.manifest)
|
|
39
|
-
puts "mounting route: #{path} => #{content_path}"
|
|
40
|
-
@server.mount_proc(path.to_s) do |_req, res|
|
|
41
|
-
res.body = File.read(content_path)
|
|
42
|
-
res.content_type = target.mime(@package.manifest)
|
|
43
|
-
end
|
|
42
|
+
paths = @routes.config.routes.map(&:path) + Introspection::PATHS
|
|
43
|
+
paths.each do |path|
|
|
44
|
+
@server.mount_proc(path.to_s) { |req, res| handle_request(req, res) }
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def
|
|
48
|
-
@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
def restart_server
|
|
49
|
+
@server.shutdown
|
|
50
|
+
@server_thread&.join
|
|
51
|
+
load_package
|
|
52
|
+
setup_server(false)
|
|
53
|
+
mount_routes
|
|
54
|
+
@server_thread = start_server
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
54
58
|
|
|
59
|
+
def setup_server(do_not_listen)
|
|
60
|
+
server_options = { Port: @port }
|
|
61
|
+
server_options[:DoNotListen] = true if do_not_listen
|
|
62
|
+
@server = WEBrick::HTTPServer.new(server_options)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def start_server
|
|
66
|
+
Thread.new do
|
|
55
67
|
puts "Starting server on http://localhost:#{@port}"
|
|
56
68
|
@server.start
|
|
57
69
|
end
|
|
58
70
|
end
|
|
59
71
|
|
|
60
|
-
def
|
|
72
|
+
def shutdown_server
|
|
73
|
+
puts "\nShutting down server..."
|
|
61
74
|
@server.shutdown
|
|
62
|
-
|
|
63
|
-
load_package
|
|
64
|
-
@server = WEBrick::HTTPServer.new(Port: @port)
|
|
65
|
-
mount_routes
|
|
66
|
-
start_server
|
|
75
|
+
exit
|
|
67
76
|
end
|
|
68
77
|
|
|
69
78
|
def start_listener
|
|
@@ -74,9 +83,72 @@ module Capsium
|
|
|
74
83
|
|
|
75
84
|
listener.start
|
|
76
85
|
puts "Listening for changes in #{@package_path}..."
|
|
77
|
-
|
|
78
|
-
# Wait for the server thread to finish
|
|
79
86
|
@server_thread.join
|
|
80
87
|
end
|
|
88
|
+
|
|
89
|
+
def load_package
|
|
90
|
+
@package = Package.new(@package_path)
|
|
91
|
+
load_state
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_state
|
|
95
|
+
@routes = @package.routes
|
|
96
|
+
@introspection = Introspection.new(@package)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def serve_introspection(request, response)
|
|
100
|
+
return respond_method_not_allowed(response) unless request.request_method == "GET"
|
|
101
|
+
|
|
102
|
+
response.status = 200
|
|
103
|
+
response["Content-Type"] = "application/json"
|
|
104
|
+
response.body = JSON.generate(@introspection.report_for(request.path))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def serve_route(route, response)
|
|
108
|
+
case route.kind
|
|
109
|
+
when :dataset then serve_dataset(route.dataset, response)
|
|
110
|
+
when :resource then serve_file(route, response)
|
|
111
|
+
else respond_not_implemented(response)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def serve_file(route, response)
|
|
116
|
+
content_path = route.fs_path(@package.path)
|
|
117
|
+
if content_path && File.exist?(content_path)
|
|
118
|
+
response.status = 200
|
|
119
|
+
response["Content-Type"] = route.mime(@package.manifest) || "application/octet-stream"
|
|
120
|
+
headers_for(route).each { |name, value| response[name] = value }
|
|
121
|
+
response.body = File.read(content_path)
|
|
122
|
+
else
|
|
123
|
+
respond_not_found(response)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def headers_for(route)
|
|
128
|
+
route.headers || (@cache_control ? { "Cache-Control" => @cache_control } : {})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def serve_dataset(dataset_name, response)
|
|
132
|
+
dataset = @package.storage.dataset(dataset_name)
|
|
133
|
+
if dataset
|
|
134
|
+
response.status = 200
|
|
135
|
+
response["Content-Type"] = "application/json"
|
|
136
|
+
response.body = JSON.generate(dataset.data)
|
|
137
|
+
else
|
|
138
|
+
respond_not_found(response)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def respond_not_found(response) = respond_text(response, 404, "Not Found")
|
|
143
|
+
|
|
144
|
+
def respond_not_implemented(response) = respond_text(response, 501, "Not Implemented")
|
|
145
|
+
|
|
146
|
+
def respond_method_not_allowed(response) = respond_text(response, 405, "Method Not Allowed")
|
|
147
|
+
|
|
148
|
+
def respond_text(response, status, body)
|
|
149
|
+
response.status = status
|
|
150
|
+
response["Content-Type"] = "text/plain"
|
|
151
|
+
response.body = body
|
|
152
|
+
end
|
|
81
153
|
end
|
|
82
154
|
end
|
data/lib/capsium/version.rb
CHANGED