mayu-live 0.0.3 → 0.0.5
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/README.md +2 -2
- data/lib/mayu/encrypted_marshal.rb +119 -0
- data/lib/mayu/environment.rb +8 -5
- data/lib/mayu/resources/generators/image.rb +7 -67
- data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +3 -3
- data/lib/mayu/resources/types/svg.rb +3 -18
- data/lib/mayu/server/app.rb +2 -2
- data/lib/mayu/server/errors.rb +2 -2
- data/lib/mayu/session.rb +3 -6
- data/lib/mayu/version.rb +1 -1
- data/mayu-live.gemspec +5 -4
- metadata +34 -14
- data/lib/mayu/message_cipher.rb +0 -172
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5814cbef30ce17db08763d4a5573090a92766e1c376e60e889ea923ec6edbd4e
|
|
4
|
+
data.tar.gz: f0ab5e3c58fdaee8dda0287dd39839b816974828e154f5d5c9423b0b1909227e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 377ade35d2d0d21a816fd942dc02dd22b488e760b888711dd892b4c8df62915254978f20671d66845faca1862d1e757ff2d9b6708e208b030bc14971695b6b93
|
|
7
|
+
data.tar.gz: 411834ef98447e08c511ba70cbdc36c87852063e74a19177e275cdab6723a3b874e495b9505d552928346fe0bb5ee97e646684acd5f7128d79bef8b6eb5c9e6c
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mayu-live/framework/actions/workflows/test.yml)
|
|
4
4
|
[](https://github.com/mayu-live/framework/releases)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
# Description
|
|
11
11
|
|
|
12
|
-
Mayu is a live
|
|
12
|
+
Mayu is a live updating server side component-based
|
|
13
13
|
VirtualDOM rendering framework written in Ruby.
|
|
14
14
|
|
|
15
15
|
Everything runs on the server, except for a tiny little runtime that
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "rbnacl"
|
|
6
|
+
require "brotli"
|
|
7
|
+
|
|
8
|
+
module Mayu
|
|
9
|
+
class EncryptedMarshal
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class IssuedInTheFutureError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ExpiredError < Error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class DecryptError < Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
AdditionalData =
|
|
23
|
+
Data.define(:issued_at, :ttl) do
|
|
24
|
+
def self.create(ttl:, now: Time.now) = new(now.to_f, ttl)
|
|
25
|
+
|
|
26
|
+
def self.unpack(data)
|
|
27
|
+
data.unpack("D S") => [issued_at, ttl]
|
|
28
|
+
new(issued_at, ttl)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def pack = [issued_at, ttl].pack("D S")
|
|
32
|
+
def expired?(now: Time.now) = now > expires_at
|
|
33
|
+
def expires_at = Time.at(issued_at + ttl)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Message =
|
|
37
|
+
Data.define(:nonce, :ad, :ciphertext) do
|
|
38
|
+
def self.unpack(data)
|
|
39
|
+
data.unpack("S a*") => [nonce_length, data]
|
|
40
|
+
data.unpack("a#{nonce_length} S a*") => [nonce, ad_length, data]
|
|
41
|
+
data.unpack("a#{ad_length} a*") => [ad, ciphertext]
|
|
42
|
+
new(nonce, AdditionalData.unpack(ad), ciphertext)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pack
|
|
46
|
+
packed_ad = ad.pack
|
|
47
|
+
[
|
|
48
|
+
nonce.bytesize,
|
|
49
|
+
nonce,
|
|
50
|
+
packed_ad.bytesize,
|
|
51
|
+
packed_ad,
|
|
52
|
+
ciphertext
|
|
53
|
+
].pack("S a* S a* a*")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def expired?(now: Time.now) = ad.expired?(now:)
|
|
57
|
+
def expires_at = ad.expires_at
|
|
58
|
+
|
|
59
|
+
def verify_timestamps!(now: Time.now)
|
|
60
|
+
if ad.expired?(now:)
|
|
61
|
+
raise ExpiredError, "Message expired at #{ad.expires_at}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if ad.issued_at > now.to_f
|
|
65
|
+
raise IssuedInTheFutureError,
|
|
66
|
+
"Message was issued in the future, #{Time.at(ad.issued_at)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
extend T::Sig
|
|
74
|
+
|
|
75
|
+
Cipher = RbNaCl::AEAD::ChaCha20Poly1305IETF
|
|
76
|
+
|
|
77
|
+
DEFAULT_TTL_SECONDS = 10
|
|
78
|
+
|
|
79
|
+
sig { returns(String) }
|
|
80
|
+
def self.random_key = RbNaCl::Random.random_bytes(Cipher::KEYBYTES)
|
|
81
|
+
|
|
82
|
+
sig { params(base_key: String, default_ttl: Integer).void }
|
|
83
|
+
def initialize(base_key, default_ttl: DEFAULT_TTL_SECONDS)
|
|
84
|
+
@cipher = Cipher.new(RbNaCl::Hash.sha256(base_key))
|
|
85
|
+
@default_ttl = default_ttl
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(object: T.untyped, ttl: Integer).returns(String) }
|
|
89
|
+
def dump(object, ttl: @default_ttl) =
|
|
90
|
+
object
|
|
91
|
+
.then { Marshal.dump(_1) }
|
|
92
|
+
.then { Brotli.deflate(_1) }
|
|
93
|
+
.then { encrypt(_1, ttl:) }
|
|
94
|
+
|
|
95
|
+
sig { params(encrypted: String).returns(T.untyped) }
|
|
96
|
+
def load(encrypted) =
|
|
97
|
+
encrypted
|
|
98
|
+
.then { Message.unpack(_1) }
|
|
99
|
+
.then { _1.verify_timestamps! }
|
|
100
|
+
.then { decrypt(_1) }
|
|
101
|
+
.then { Brotli.inflate(_1) }
|
|
102
|
+
.then { Marshal.load(_1) }
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def encrypt(message, ttl:)
|
|
107
|
+
nonce = RbNaCl::Random.random_bytes(@cipher.nonce_bytes)
|
|
108
|
+
ad = AdditionalData.create(ttl:)
|
|
109
|
+
ciphertext = @cipher.encrypt(nonce, message, ad.pack)
|
|
110
|
+
Message.new(nonce, ad, ciphertext).pack
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def decrypt(message)
|
|
114
|
+
@cipher.decrypt(message.nonce, message.ciphertext, message.ad.pack)
|
|
115
|
+
rescue RbNaCl::CryptoError => e
|
|
116
|
+
raise DecryptError, e.message
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/mayu/environment.rb
CHANGED
|
@@ -10,7 +10,7 @@ require_relative "metrics"
|
|
|
10
10
|
require_relative "app_metrics"
|
|
11
11
|
require_relative "resources/registry"
|
|
12
12
|
require_relative "fetch"
|
|
13
|
-
require_relative "
|
|
13
|
+
require_relative "encrypted_marshal"
|
|
14
14
|
require_relative "configuration"
|
|
15
15
|
|
|
16
16
|
module Mayu
|
|
@@ -34,8 +34,8 @@ module Mayu
|
|
|
34
34
|
attr_reader :resources
|
|
35
35
|
sig { returns(Fetch) }
|
|
36
36
|
attr_reader :fetch
|
|
37
|
-
sig { returns(
|
|
38
|
-
attr_reader :
|
|
37
|
+
sig { returns(EncryptedMarshal) }
|
|
38
|
+
attr_reader :encrypted_marshal
|
|
39
39
|
sig { returns(AppMetrics) }
|
|
40
40
|
attr_reader :metrics
|
|
41
41
|
|
|
@@ -44,8 +44,11 @@ module Mayu
|
|
|
44
44
|
@root = T.let(config.root, String)
|
|
45
45
|
@app_root = T.let(File.join(config.root, "app"), String)
|
|
46
46
|
@config = config
|
|
47
|
-
@
|
|
48
|
-
T.let(
|
|
47
|
+
@encrypted_marshal =
|
|
48
|
+
T.let(
|
|
49
|
+
EncryptedMarshal.new(config.secret_key, default_ttl: 30),
|
|
50
|
+
EncryptedMarshal
|
|
51
|
+
)
|
|
49
52
|
# TODO: Reload routes when things change in /pages...
|
|
50
53
|
# Should probably make routes into a resource type.
|
|
51
54
|
@routes =
|
|
@@ -27,78 +27,18 @@ module Mayu
|
|
|
27
27
|
def process(target_path)
|
|
28
28
|
return if File.exist?(target_path)
|
|
29
29
|
|
|
30
|
+
require "rmagick"
|
|
31
|
+
|
|
30
32
|
Console.logger.info(
|
|
31
33
|
self,
|
|
32
34
|
"Generating #{target_path} from #{@source_path}"
|
|
33
35
|
)
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
width: @version.width,
|
|
41
|
-
target_path: target_path
|
|
42
|
-
)
|
|
43
|
-
else
|
|
44
|
-
convert_generic(
|
|
45
|
-
source_path: @source_path,
|
|
46
|
-
quality: 80, # typical quality,
|
|
47
|
-
width: @version.width,
|
|
48
|
-
target_path: target_path
|
|
49
|
-
)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
sig do
|
|
56
|
-
params(
|
|
57
|
-
source_path: String,
|
|
58
|
-
quality: Integer,
|
|
59
|
-
width: Integer,
|
|
60
|
-
target_path: String
|
|
61
|
-
).void
|
|
62
|
-
end
|
|
63
|
-
def convert_webp(source_path:, quality:, width:, target_path:)
|
|
64
|
-
system(
|
|
65
|
-
Shellwords.shelljoin(
|
|
66
|
-
[
|
|
67
|
-
"cwebp",
|
|
68
|
-
"-q",
|
|
69
|
-
"#{quality}",
|
|
70
|
-
"-resize",
|
|
71
|
-
"#{width}",
|
|
72
|
-
"0",
|
|
73
|
-
source_path,
|
|
74
|
-
"-o",
|
|
75
|
-
target_path
|
|
76
|
-
]
|
|
77
|
-
) + " 2> /dev/null"
|
|
78
|
-
) or raise "Could not generate #{target_path} from #{source_path}"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
sig do
|
|
82
|
-
params(
|
|
83
|
-
source_path: String,
|
|
84
|
-
quality: Integer,
|
|
85
|
-
width: Integer,
|
|
86
|
-
target_path: String
|
|
87
|
-
).void
|
|
88
|
-
end
|
|
89
|
-
def convert_generic(source_path:, quality:, width:, target_path:)
|
|
90
|
-
system(
|
|
91
|
-
Shellwords.shelljoin(
|
|
92
|
-
[
|
|
93
|
-
"convert",
|
|
94
|
-
source_path,
|
|
95
|
-
"-adaptive-resize",
|
|
96
|
-
"#{width}",
|
|
97
|
-
"-strip",
|
|
98
|
-
target_path
|
|
99
|
-
]
|
|
100
|
-
) + " 2> /dev/null"
|
|
101
|
-
) or raise "Could not generate #{target_path} from #{source_path}"
|
|
37
|
+
Magick::Image
|
|
38
|
+
.read(@source_path)
|
|
39
|
+
.first
|
|
40
|
+
.resize_to_fit(@version.width)
|
|
41
|
+
.write(target_path) { |options| options.quality = 80 }
|
|
102
42
|
end
|
|
103
43
|
end
|
|
104
44
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
@layer app\/components\/MyComponent\?
|
|
2
|
-
@media (
|
|
1
|
+
@layer app\/components\/MyComponent\?E59aOM9B {
|
|
2
|
+
@media (width >= 8em) and (width <= 32em) {
|
|
3
3
|
.app\/components\/MyComponent\.foo\?Ef7fDeuq {
|
|
4
4
|
color: #f0f;
|
|
5
5
|
}
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
color: #00f;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
}
|
|
12
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# typed: strict
|
|
3
3
|
|
|
4
|
-
require "svg_optimizer"
|
|
5
4
|
require_relative "base"
|
|
6
5
|
|
|
7
6
|
module Mayu
|
|
@@ -10,30 +9,16 @@ module Mayu
|
|
|
10
9
|
class SVG < Base
|
|
11
10
|
extend T::Sig
|
|
12
11
|
|
|
13
|
-
SVG_OPTIMIZER_PLUGINS =
|
|
14
|
-
T.let(
|
|
15
|
-
SvgOptimizer::DEFAULT_PLUGINS -
|
|
16
|
-
[
|
|
17
|
-
# The following plugin sets fill="none" in some cases
|
|
18
|
-
# which breaks the fontawesome icons... That's why
|
|
19
|
-
# it's disabled...
|
|
20
|
-
SvgOptimizer::Plugins::RemoveUselessStrokeAndFill
|
|
21
|
-
],
|
|
22
|
-
T::Array[T.class_of(SvgOptimizer::Plugins::Base)]
|
|
23
|
-
)
|
|
24
|
-
|
|
25
12
|
sig { params(resource: Resource).void }
|
|
26
13
|
def initialize(resource)
|
|
27
14
|
@resource = resource
|
|
28
15
|
|
|
29
|
-
|
|
30
|
-
optimized = SvgOptimizer.optimize(original, SVG_OPTIMIZER_PLUGINS)
|
|
16
|
+
source = resource.read(encoding: "utf-8")
|
|
31
17
|
|
|
32
|
-
content_hash =
|
|
33
|
-
Base64.urlsafe_encode64(Digest::SHA256.digest(optimized))
|
|
18
|
+
content_hash = Base64.urlsafe_encode64(Digest::SHA256.digest(source))
|
|
34
19
|
|
|
35
20
|
@filename = T.let("#{content_hash}.svg", String)
|
|
36
|
-
@source = T.let(
|
|
21
|
+
@source = T.let(source, String)
|
|
37
22
|
end
|
|
38
23
|
|
|
39
24
|
sig { returns(T::Array[Asset]) }
|
data/lib/mayu/server/app.rb
CHANGED
|
@@ -433,7 +433,7 @@ module Mayu
|
|
|
433
433
|
)
|
|
434
434
|
end
|
|
435
435
|
|
|
436
|
-
@environment.
|
|
436
|
+
@environment.encrypted_marshal.load(body) => String => dumped
|
|
437
437
|
session = Session.restore(environment: @environment, dumped:)
|
|
438
438
|
@sessions.store(session.id, session)
|
|
439
439
|
end
|
|
@@ -454,7 +454,7 @@ module Mayu
|
|
|
454
454
|
EventStream::Message.new(
|
|
455
455
|
:"session.transfer",
|
|
456
456
|
EventStream::Blob.new(
|
|
457
|
-
@environment.
|
|
457
|
+
@environment.encrypted_marshal.dump(
|
|
458
458
|
Session::SerializedSession.dump_session(session)
|
|
459
459
|
)
|
|
460
460
|
)
|
data/lib/mayu/server/errors.rb
CHANGED
|
@@ -64,9 +64,9 @@ module Mayu
|
|
|
64
64
|
text_response(403, "session cookie not set")
|
|
65
65
|
rescue SessionNotFound => e
|
|
66
66
|
text_response(404, "session not found")
|
|
67
|
-
rescue Mayu::
|
|
67
|
+
rescue Mayu::EncryptedMarshal::DecryptError => e
|
|
68
68
|
text_response(403, "decrypt error")
|
|
69
|
-
rescue Mayu::
|
|
69
|
+
rescue Mayu::EncryptedMarshal::ExpiredError => e
|
|
70
70
|
text_response(403, "session expired")
|
|
71
71
|
rescue InvalidToken => e
|
|
72
72
|
text_response(403, "invalid token")
|
data/lib/mayu/session.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
require "nanoid"
|
|
5
|
+
require "rbnacl"
|
|
5
6
|
require_relative "environment"
|
|
6
7
|
require_relative "vdom/vtree"
|
|
7
8
|
require_relative "vdom/marshalling"
|
|
@@ -77,11 +78,7 @@ module Mayu
|
|
|
77
78
|
|
|
78
79
|
sig { params(token: String).returns(T::Boolean) }
|
|
79
80
|
def authorized?(token)
|
|
80
|
-
|
|
81
|
-
OpenSSL.fixed_length_secure_compare(self.token, token)
|
|
82
|
-
else
|
|
83
|
-
false
|
|
84
|
-
end
|
|
81
|
+
RbNaCl::Util.verify64(self.token, token)
|
|
85
82
|
end
|
|
86
83
|
|
|
87
84
|
Marshaled = T.type_alias { [String, String, String, String, String] }
|
|
@@ -166,7 +163,7 @@ module Mayu
|
|
|
166
163
|
# freeze
|
|
167
164
|
|
|
168
165
|
# encrypted_session =
|
|
169
|
-
# @environment.
|
|
166
|
+
# @environment.encrypted_marshal.dump(SerializedSession.dump_session(self))
|
|
170
167
|
|
|
171
168
|
links = [
|
|
172
169
|
%{<script async type="module" src="/__mayu/runtime/#{environment.init_js}##{id}" crossorigin="same-origin" fetchpriority="high"></script>},
|
data/lib/mayu/version.rb
CHANGED
data/mayu-live.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.summary = "Server side VDOM framework"
|
|
12
12
|
|
|
13
13
|
spec.description = <<~EOF
|
|
14
|
-
Mayu Live is a live
|
|
14
|
+
Mayu Live is a live updating server side VirtualDOM framework for Ruby,
|
|
15
15
|
inspired by modern frontend tools that exist in the JavaScript ecosystem.
|
|
16
16
|
EOF
|
|
17
17
|
|
|
@@ -53,8 +53,9 @@ Gem::Specification.new do |spec|
|
|
|
53
53
|
spec.add_dependency "prometheus-client", "~> 4.0.0"
|
|
54
54
|
spec.add_dependency "protocol-http", "~> 0.23.12"
|
|
55
55
|
spec.add_dependency "pry", "~> 0.14.2"
|
|
56
|
-
spec.add_dependency "rack", "
|
|
56
|
+
spec.add_dependency "rack", ">= 3.0.4.1", "< 3.0.9.0"
|
|
57
57
|
spec.add_dependency "rake", "~> 13.0.6"
|
|
58
|
+
spec.add_dependency "rbnacl", "~> 7.1.1"
|
|
58
59
|
spec.add_dependency "sorbet-runtime", "~> 0.5.10634"
|
|
59
60
|
spec.add_dependency "terminal-table", "~> 3.0.2"
|
|
60
61
|
spec.add_dependency "toml-rb", "~> 2.2.0"
|
|
@@ -67,9 +68,9 @@ Gem::Specification.new do |spec|
|
|
|
67
68
|
spec.add_dependency "image_size", "~> 3.2.0"
|
|
68
69
|
spec.add_dependency "kramdown", "~> 2.4.0"
|
|
69
70
|
spec.add_dependency "rouge", "~> 4.0.0"
|
|
70
|
-
spec.add_dependency "mayu-css", "~> 0.0.
|
|
71
|
+
spec.add_dependency "mayu-css", "~> 0.0.3"
|
|
72
|
+
spec.add_dependency "rmagick", "~> 5.3.0"
|
|
71
73
|
spec.add_dependency "source_map", "~> 3.0.1"
|
|
72
|
-
spec.add_dependency "svg_optimizer", "~> 0.2.6"
|
|
73
74
|
spec.add_dependency "syntax_tree", "~> 5.3.0"
|
|
74
75
|
spec.add_dependency "syntax_tree-haml", "~> 3.0.0"
|
|
75
76
|
spec.add_dependency "syntax_tree-xml", "~> 0.1.0"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mayu-live
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andreas Alin
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-12-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -154,16 +154,22 @@ dependencies:
|
|
|
154
154
|
name: rack
|
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
|
156
156
|
requirements:
|
|
157
|
-
- - "
|
|
157
|
+
- - ">="
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
159
|
version: 3.0.4.1
|
|
160
|
+
- - "<"
|
|
161
|
+
- !ruby/object:Gem::Version
|
|
162
|
+
version: 3.0.9.0
|
|
160
163
|
type: :runtime
|
|
161
164
|
prerelease: false
|
|
162
165
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
166
|
requirements:
|
|
164
|
-
- - "
|
|
167
|
+
- - ">="
|
|
165
168
|
- !ruby/object:Gem::Version
|
|
166
169
|
version: 3.0.4.1
|
|
170
|
+
- - "<"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: 3.0.9.0
|
|
167
173
|
- !ruby/object:Gem::Dependency
|
|
168
174
|
name: rake
|
|
169
175
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -178,6 +184,20 @@ dependencies:
|
|
|
178
184
|
- - "~>"
|
|
179
185
|
- !ruby/object:Gem::Version
|
|
180
186
|
version: 13.0.6
|
|
187
|
+
- !ruby/object:Gem::Dependency
|
|
188
|
+
name: rbnacl
|
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - "~>"
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: 7.1.1
|
|
194
|
+
type: :runtime
|
|
195
|
+
prerelease: false
|
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - "~>"
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: 7.1.1
|
|
181
201
|
- !ruby/object:Gem::Dependency
|
|
182
202
|
name: sorbet-runtime
|
|
183
203
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -296,42 +316,42 @@ dependencies:
|
|
|
296
316
|
requirements:
|
|
297
317
|
- - "~>"
|
|
298
318
|
- !ruby/object:Gem::Version
|
|
299
|
-
version: 0.0.
|
|
319
|
+
version: 0.0.3
|
|
300
320
|
type: :runtime
|
|
301
321
|
prerelease: false
|
|
302
322
|
version_requirements: !ruby/object:Gem::Requirement
|
|
303
323
|
requirements:
|
|
304
324
|
- - "~>"
|
|
305
325
|
- !ruby/object:Gem::Version
|
|
306
|
-
version: 0.0.
|
|
326
|
+
version: 0.0.3
|
|
307
327
|
- !ruby/object:Gem::Dependency
|
|
308
|
-
name:
|
|
328
|
+
name: rmagick
|
|
309
329
|
requirement: !ruby/object:Gem::Requirement
|
|
310
330
|
requirements:
|
|
311
331
|
- - "~>"
|
|
312
332
|
- !ruby/object:Gem::Version
|
|
313
|
-
version: 3.0
|
|
333
|
+
version: 5.3.0
|
|
314
334
|
type: :runtime
|
|
315
335
|
prerelease: false
|
|
316
336
|
version_requirements: !ruby/object:Gem::Requirement
|
|
317
337
|
requirements:
|
|
318
338
|
- - "~>"
|
|
319
339
|
- !ruby/object:Gem::Version
|
|
320
|
-
version: 3.0
|
|
340
|
+
version: 5.3.0
|
|
321
341
|
- !ruby/object:Gem::Dependency
|
|
322
|
-
name:
|
|
342
|
+
name: source_map
|
|
323
343
|
requirement: !ruby/object:Gem::Requirement
|
|
324
344
|
requirements:
|
|
325
345
|
- - "~>"
|
|
326
346
|
- !ruby/object:Gem::Version
|
|
327
|
-
version: 0.
|
|
347
|
+
version: 3.0.1
|
|
328
348
|
type: :runtime
|
|
329
349
|
prerelease: false
|
|
330
350
|
version_requirements: !ruby/object:Gem::Requirement
|
|
331
351
|
requirements:
|
|
332
352
|
- - "~>"
|
|
333
353
|
- !ruby/object:Gem::Version
|
|
334
|
-
version: 0.
|
|
354
|
+
version: 3.0.1
|
|
335
355
|
- !ruby/object:Gem::Dependency
|
|
336
356
|
name: syntax_tree
|
|
337
357
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -375,7 +395,7 @@ dependencies:
|
|
|
375
395
|
- !ruby/object:Gem::Version
|
|
376
396
|
version: 0.1.0
|
|
377
397
|
description: |
|
|
378
|
-
Mayu Live is a live
|
|
398
|
+
Mayu Live is a live updating server side VirtualDOM framework for Ruby,
|
|
379
399
|
inspired by modern frontend tools that exist in the JavaScript ecosystem.
|
|
380
400
|
email:
|
|
381
401
|
- andreas.alin@gmail.com
|
|
@@ -450,12 +470,12 @@ files:
|
|
|
450
470
|
- lib/mayu/component/wrapper.rb
|
|
451
471
|
- lib/mayu/configuration.rb
|
|
452
472
|
- lib/mayu/disable_sorbet.rb
|
|
473
|
+
- lib/mayu/encrypted_marshal.rb
|
|
453
474
|
- lib/mayu/environment.rb
|
|
454
475
|
- lib/mayu/event_stream.rb
|
|
455
476
|
- lib/mayu/fetch.rb
|
|
456
477
|
- lib/mayu/html.rb
|
|
457
478
|
- lib/mayu/html.yaml
|
|
458
|
-
- lib/mayu/message_cipher.rb
|
|
459
479
|
- lib/mayu/metrics.rb
|
|
460
480
|
- lib/mayu/metrics/collector.rb
|
|
461
481
|
- lib/mayu/metrics/exporter.rb
|
data/lib/mayu/message_cipher.rb
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# typed: strict
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "sorbet-runtime"
|
|
5
|
-
require "time"
|
|
6
|
-
require "digest/sha2"
|
|
7
|
-
require "openssl"
|
|
8
|
-
require "securerandom"
|
|
9
|
-
require "brotli"
|
|
10
|
-
|
|
11
|
-
module Mayu
|
|
12
|
-
class MessageCipher
|
|
13
|
-
extend T::Sig
|
|
14
|
-
|
|
15
|
-
DEFAULT_TTL_SECONDS = T.let(10, Integer)
|
|
16
|
-
|
|
17
|
-
Message = T.type_alias { { iss: Float, exp: Float, payload: T.untyped } }
|
|
18
|
-
|
|
19
|
-
class Error < StandardError
|
|
20
|
-
end
|
|
21
|
-
class ExpiredError < Error
|
|
22
|
-
end
|
|
23
|
-
class IssuedInTheFutureError < Error
|
|
24
|
-
end
|
|
25
|
-
class EncryptError < Error
|
|
26
|
-
end
|
|
27
|
-
class DecryptError < Error
|
|
28
|
-
end
|
|
29
|
-
class InvalidHMACError < Error
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
sig { params(key: String, ttl: Integer).void }
|
|
33
|
-
def initialize(key:, ttl: DEFAULT_TTL_SECONDS)
|
|
34
|
-
raise ArgumentError, "ttl must be positive" unless ttl.positive?
|
|
35
|
-
@default_ttl_seconds = ttl
|
|
36
|
-
@key = T.let(Digest::SHA256.digest(key), String)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
sig do
|
|
40
|
-
params(payload: T.untyped, auth_data: String, ttl: Integer).returns(
|
|
41
|
-
String
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
def dump(payload, auth_data: "", ttl: @default_ttl_seconds)
|
|
45
|
-
raise ArgumentError, "ttl must be positive" unless ttl.positive?
|
|
46
|
-
now = Time.now.to_f
|
|
47
|
-
message = { iss: now, exp: now + ttl, payload: Marshal.dump(payload) }
|
|
48
|
-
encode_message(message, auth_data:)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
sig { params(data: String, auth_data: String).returns(T.untyped) }
|
|
52
|
-
def load(data, auth_data: "")
|
|
53
|
-
Marshal.load(decode_message(data, auth_data:))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
sig { params(message: Message, auth_data: String).returns(String) }
|
|
59
|
-
def encode_message(message, auth_data: "")
|
|
60
|
-
message
|
|
61
|
-
.then { Marshal.dump(_1) }
|
|
62
|
-
.then { prepend_hmac(_1) }
|
|
63
|
-
.then { Brotli.deflate(_1) }
|
|
64
|
-
.then { encrypt(_1, auth_data:) }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
sig { params(message: String, auth_data: String).returns(String) }
|
|
68
|
-
def decode_message(message, auth_data: "")
|
|
69
|
-
message
|
|
70
|
-
.then { decrypt(_1, auth_data:) }
|
|
71
|
-
.then { Brotli.inflate(_1) }
|
|
72
|
-
.then { validate_hmac(_1) }
|
|
73
|
-
.then { Marshal.load(_1) }
|
|
74
|
-
.tap { validate_times(_1) }
|
|
75
|
-
.fetch(:payload)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
sig { params(input: String).returns(String) }
|
|
79
|
-
def prepend_hmac(input)
|
|
80
|
-
hmac = Digest::SHA256.digest(input)
|
|
81
|
-
input.prepend(hmac)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
sig { params(input: String).returns(String) }
|
|
85
|
-
def validate_hmac(input)
|
|
86
|
-
hmac, message = input.unpack("a32 a*")
|
|
87
|
-
|
|
88
|
-
unless OpenSSL.fixed_length_secure_compare(
|
|
89
|
-
hmac,
|
|
90
|
-
Digest::SHA256.digest(message.to_s)
|
|
91
|
-
)
|
|
92
|
-
raise InvalidHMACError
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
message.to_s
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
sig { params(message: { iss: Float, exp: Float, payload: String }).void }
|
|
99
|
-
def validate_times(message)
|
|
100
|
-
message => { iss:, exp: }
|
|
101
|
-
now = Time.now.to_f
|
|
102
|
-
validate_iss(now, iss)
|
|
103
|
-
validate_exp(now, exp)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
sig { params(now: Float, iss: Float).void }
|
|
107
|
-
def validate_iss(now, iss)
|
|
108
|
-
return if iss < now
|
|
109
|
-
|
|
110
|
-
raise IssuedInTheFutureError,
|
|
111
|
-
"The message was issued at #{Time.at(iss).iso8601}, which is in the future"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
sig { params(now: Float, exp: Float).void }
|
|
115
|
-
def validate_exp(now, exp)
|
|
116
|
-
return if exp > now
|
|
117
|
-
|
|
118
|
-
raise ExpiredError,
|
|
119
|
-
"The message expired at #{Time.at(exp).iso8601}, which is in the past"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
sig { params(message: String, auth_data: String).returns(String) }
|
|
123
|
-
def encrypt(message, auth_data: "")
|
|
124
|
-
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
125
|
-
cipher.encrypt
|
|
126
|
-
salt = SecureRandom.random_bytes(8)
|
|
127
|
-
cipher.key = generate_key(salt)
|
|
128
|
-
cipher.iv = iv = cipher.random_iv
|
|
129
|
-
cipher.auth_data = auth_data
|
|
130
|
-
cipher_text = cipher.update(message) + cipher.final
|
|
131
|
-
auth_tag = cipher.auth_tag
|
|
132
|
-
[auth_tag.bytesize, auth_tag, salt, iv, cipher_text].pack("C a* a* a* a*")
|
|
133
|
-
rescue OpenSSL::Cipher::CipherError
|
|
134
|
-
raise EncryptError
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
sig { params(data: String, auth_data: String).returns(String) }
|
|
138
|
-
def decrypt(data, auth_data: "")
|
|
139
|
-
data.unpack("C a*") => [Integer => auth_tag_len, String => data]
|
|
140
|
-
|
|
141
|
-
data.unpack("a#{auth_tag_len} a8 a12 a*") => [
|
|
142
|
-
auth_tag,
|
|
143
|
-
salt,
|
|
144
|
-
iv,
|
|
145
|
-
cipher_text
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
149
|
-
cipher.iv = iv
|
|
150
|
-
cipher.key = generate_key(salt)
|
|
151
|
-
cipher.auth_data = auth_data
|
|
152
|
-
cipher.auth_tag = auth_tag
|
|
153
|
-
cipher.update(cipher_text) + cipher.final
|
|
154
|
-
rescue NoMatchingPatternError
|
|
155
|
-
raise DecryptError
|
|
156
|
-
rescue OpenSSL::Cipher::CipherError
|
|
157
|
-
raise DecryptError
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
sig { params(salt: String).returns(String) }
|
|
161
|
-
def generate_key(salt)
|
|
162
|
-
OpenSSL::KDF.scrypt(
|
|
163
|
-
@key,
|
|
164
|
-
salt:, # Salt.
|
|
165
|
-
N: 2**14, # CPU/memory cost parameter. This must be a power of 2.
|
|
166
|
-
r: 8, # Block size parameter.
|
|
167
|
-
p: 1, # Parallelization parameter
|
|
168
|
-
length: 32 # Length in octets of the derived key
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|