tina4ruby 3.13.137 → 3.13.138
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/CHANGELOG.md +36 -0
- data/COMMERCIAL-LICENSE.md +8 -0
- data/LICENSE +373 -0
- data/LICENSE.txt +373 -21
- data/NOTICE +11 -0
- data/README.md +11 -7
- data/exe/tina4ruby +6 -0
- data/lib/tina4/ai.rb +6 -0
- data/lib/tina4/ai_client.rb +10 -3
- data/lib/tina4/api.rb +57 -7
- data/lib/tina4/auth.rb +18 -12
- data/lib/tina4/auto_crud.rb +83 -27
- data/lib/tina4/background.rb +7 -1
- data/lib/tina4/base64.rb +59 -0
- data/lib/tina4/cache.rb +6 -0
- data/lib/tina4/cache_backends/base_backend.rb +6 -0
- data/lib/tina4/cache_backends/database_backend.rb +6 -0
- data/lib/tina4/cache_backends/file_backend.rb +6 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +6 -0
- data/lib/tina4/cache_backends/memory_backend.rb +6 -0
- data/lib/tina4/cache_backends/mongo_backend.rb +6 -0
- data/lib/tina4/cache_backends/redis_backend.rb +6 -0
- data/lib/tina4/cache_backends/valkey_backend.rb +6 -0
- data/lib/tina4/cache_backends.rb +6 -0
- data/lib/tina4/cli.rb +22 -7
- data/lib/tina4/constants.rb +12 -8
- data/lib/tina4/container.rb +6 -0
- data/lib/tina4/context/chunker.rb +6 -0
- data/lib/tina4/context.rb +13 -5
- data/lib/tina4/cors.rb +66 -10
- data/lib/tina4/crud.rb +78 -9
- data/lib/tina4/database/sqlite3_adapter.rb +9 -1
- data/lib/tina4/database.rb +260 -44
- data/lib/tina4/database_adapter.rb +6 -0
- data/lib/tina4/database_result.rb +6 -0
- data/lib/tina4/database_url.rb +6 -0
- data/lib/tina4/debug.rb +6 -0
- data/lib/tina4/dev.rb +6 -0
- data/lib/tina4/dev_admin.rb +215 -89
- data/lib/tina4/dev_mailbox.rb +6 -1
- data/lib/tina4/dispatch_pipeline.rb +86 -1
- data/lib/tina4/docs.rb +6 -0
- data/lib/tina4/docstore.rb +23 -4
- data/lib/tina4/drivers/arango_graph_driver.rb +8 -1
- data/lib/tina4/drivers/bolt_graph_driver.rb +6 -0
- data/lib/tina4/drivers/firebird_driver.rb +95 -6
- data/lib/tina4/drivers/mongodb_driver.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +54 -39
- data/lib/tina4/drivers/mysql_driver.rb +6 -0
- data/lib/tina4/drivers/odbc_driver.rb +6 -0
- data/lib/tina4/drivers/postgres_driver.rb +35 -8
- data/lib/tina4/drivers/schema_split.rb +6 -0
- data/lib/tina4/drivers/sqlite_driver.rb +8 -1
- data/lib/tina4/drivers/ultipa_graph_driver.rb +6 -0
- data/lib/tina4/env.rb +6 -0
- data/lib/tina4/error_overlay.rb +6 -0
- data/lib/tina4/events.rb +6 -0
- data/lib/tina4/feedback.rb +9 -2
- data/lib/tina4/field_types.rb +6 -0
- data/lib/tina4/form_parser.rb +241 -0
- data/lib/tina4/frond.rb +196 -56
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +6 -0
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +6 -0
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +6 -0
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +6 -0
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +6 -0
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +6 -0
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +6 -0
- data/lib/tina4/graph.rb +6 -0
- data/lib/tina4/graphql.rb +28 -27
- data/lib/tina4/health.rb +6 -0
- data/lib/tina4/html_element.rb +6 -0
- data/lib/tina4/http_server.rb +690 -0
- data/lib/tina4/imap_client.rb +352 -0
- data/lib/tina4/import_helper.rb +6 -0
- data/lib/tina4/job.rb +6 -0
- data/lib/tina4/localization.rb +6 -0
- data/lib/tina4/log.rb +6 -0
- data/lib/tina4/mail_socket.rb +124 -0
- data/lib/tina4/mcp.rb +15 -8
- data/lib/tina4/messenger.rb +86 -55
- data/lib/tina4/metrics.rb +6 -0
- data/lib/tina4/middleware.rb +24 -9
- data/lib/tina4/migration.rb +6 -0
- data/lib/tina4/model_collection.rb +6 -0
- data/lib/tina4/mqtt.rb +9 -2
- data/lib/tina4/mqtt_message.rb +6 -0
- data/lib/tina4/orm.rb +72 -16
- data/lib/tina4/parse_json.rb +25 -0
- data/lib/tina4/plan.rb +9 -2
- data/lib/tina4/point.rb +6 -0
- data/lib/tina4/port_takeover.rb +6 -0
- data/lib/tina4/project_index.rb +6 -0
- data/lib/tina4/public/THIRD-PARTY-NOTICES.md +55 -0
- data/lib/tina4/public/bundled-component-provenance.json +48 -0
- data/lib/tina4/public/js/tina4js.min.js +13 -3
- data/lib/tina4/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- data/lib/tina4/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- data/lib/tina4/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- data/lib/tina4/public/licenses/tina4-css-MIT.txt +21 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-components.json +240 -0
- data/lib/tina4/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- data/lib/tina4/public/licenses/tina4-js-MIT.txt +20 -0
- data/lib/tina4/push.rb +9 -3
- data/lib/tina4/query_builder.rb +6 -0
- data/lib/tina4/queue.rb +6 -0
- data/lib/tina4/queue_backends/kafka_backend.rb +10 -2
- data/lib/tina4/queue_backends/lite_backend.rb +6 -0
- data/lib/tina4/queue_backends/mongo_backend.rb +6 -0
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +10 -2
- data/lib/tina4/rack_app.rb +11 -1
- data/lib/tina4/rate_limiter.rb +6 -0
- data/lib/tina4/realtime/attachment.rb +6 -0
- data/lib/tina4/realtime/channel.rb +6 -0
- data/lib/tina4/realtime/channel_member.rb +6 -0
- data/lib/tina4/realtime/local_storage.rb +6 -0
- data/lib/tina4/realtime/message.rb +6 -0
- data/lib/tina4/realtime/s3_storage.rb +6 -0
- data/lib/tina4/realtime/storage.rb +6 -0
- data/lib/tina4/realtime/storage_backend.rb +6 -0
- data/lib/tina4/realtime/workspace.rb +6 -0
- data/lib/tina4/realtime.rb +10 -3
- data/lib/tina4/request.rb +85 -127
- data/lib/tina4/response.rb +71 -9
- data/lib/tina4/response_cache.rb +6 -0
- data/lib/tina4/router.rb +6 -0
- data/lib/tina4/secret_file.rb +34 -0
- data/lib/tina4/seeder.rb +9 -3
- data/lib/tina4/service.rb +8 -2
- data/lib/tina4/service_runner.rb +6 -0
- data/lib/tina4/session.rb +6 -0
- data/lib/tina4/session_handlers/database_handler.rb +6 -0
- data/lib/tina4/session_handlers/file_handler.rb +6 -0
- data/lib/tina4/session_handlers/memcached_handler.rb +6 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +108 -39
- data/lib/tina4/session_handlers/mongo_wire_client.rb +23 -9
- data/lib/tina4/session_handlers/redis_handler.rb +6 -0
- data/lib/tina4/session_handlers/resp_client.rb +6 -0
- data/lib/tina4/session_handlers/valkey_handler.rb +6 -0
- data/lib/tina4/shutdown.rb +17 -14
- data/lib/tina4/smtp_client.rb +222 -0
- data/lib/tina4/sql_translator.rb +93 -0
- data/lib/tina4/sqlite3_gem.rb +27 -0
- data/lib/tina4/sso.rb +25 -8
- data/lib/tina4/swagger.rb +6 -0
- data/lib/tina4/template.rb +6 -0
- data/lib/tina4/test.rb +8 -2
- data/lib/tina4/test_client.rb +8 -1
- data/lib/tina4/testing.rb +8 -1
- data/lib/tina4/validator.rb +6 -0
- data/lib/tina4/version.rb +7 -1
- data/lib/tina4/webserver.rb +25 -143
- data/lib/tina4/websocket.rb +10 -4
- data/lib/tina4/websocket_backplane.rb +19 -2
- data/lib/tina4/wsdl.rb +31 -16
- data/lib/tina4/xml_parser.rb +259 -0
- data/lib/tina4.rb +87 -31
- data/lib/tina4ruby.rb +6 -0
- metadata +43 -140
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
data/lib/tina4/push.rb
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "openssl"
|
|
4
|
-
|
|
10
|
+
require_relative "base64"
|
|
5
11
|
require "json"
|
|
6
12
|
require "net/http"
|
|
7
13
|
require "uri"
|
|
@@ -113,13 +119,13 @@ module Tina4
|
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
def self.b64(value)
|
|
116
|
-
Base64.urlsafe_encode64(value, padding: false)
|
|
122
|
+
Tina4::Base64.urlsafe_encode64(value, padding: false)
|
|
117
123
|
end
|
|
118
124
|
|
|
119
125
|
def decode(value, name)
|
|
120
126
|
raise PushError, "#{name} must be a non-empty base64url string" unless value.is_a?(String) && value.match?(/\A[A-Za-z0-9_-]+\z/)
|
|
121
127
|
|
|
122
|
-
Base64.urlsafe_decode64(value)
|
|
128
|
+
Tina4::Base64.urlsafe_decode64(value)
|
|
123
129
|
rescue ArgumentError => e
|
|
124
130
|
raise PushError, "#{name} must be base64url encoded: #{e.message}"
|
|
125
131
|
end
|
data/lib/tina4/query_builder.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
# QueryBuilder — Fluent SQL query builder.
|
data/lib/tina4/queue.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "securerandom"
|
|
4
10
|
require "uri"
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require_relative "../parse_json"
|
|
2
10
|
|
|
3
11
|
module Tina4
|
|
4
12
|
module QueueBackends
|
|
@@ -120,7 +128,7 @@ module Tina4
|
|
|
120
128
|
end
|
|
121
129
|
return nil unless msg
|
|
122
130
|
|
|
123
|
-
data =
|
|
131
|
+
data = Tina4.parse_json(msg.payload)
|
|
124
132
|
@last_message = msg
|
|
125
133
|
|
|
126
134
|
# attempts and error MUST be carried back. Rebuilding from
|
|
@@ -243,7 +251,7 @@ module Tina4
|
|
|
243
251
|
loop do
|
|
244
252
|
msg = reader.poll(500)
|
|
245
253
|
if msg
|
|
246
|
-
data =
|
|
254
|
+
data = Tina4.parse_json(msg.payload)
|
|
247
255
|
data["status"] = "dead"
|
|
248
256
|
out << data
|
|
249
257
|
next
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "fileutils"
|
|
4
10
|
require "time"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module QueueBackends
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
require_relative "../parse_json"
|
|
2
10
|
|
|
3
11
|
module Tina4
|
|
4
12
|
module QueueBackends
|
|
@@ -62,7 +70,7 @@ module Tina4
|
|
|
62
70
|
delivery_info, _properties, payload = queue.pop(manual_ack: true)
|
|
63
71
|
return nil unless payload
|
|
64
72
|
|
|
65
|
-
data =
|
|
73
|
+
data = Tina4.parse_json(payload)
|
|
66
74
|
# attempts and error MUST be carried back. Rebuilding the Job from
|
|
67
75
|
# topic/payload/id alone reset attempts to 0 on every redelivery, so
|
|
68
76
|
# fail()'s attempts >= max_retries check could never trip and a poison
|
|
@@ -282,7 +290,7 @@ module Tina4
|
|
|
282
290
|
_info, _props, payload = queue.pop(manual_ack: false)
|
|
283
291
|
break unless payload
|
|
284
292
|
|
|
285
|
-
out <<
|
|
293
|
+
out << Tina4.parse_json(payload)
|
|
286
294
|
end
|
|
287
295
|
out.each { |data| publish_to(name, data) }
|
|
288
296
|
out
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "json"
|
|
3
9
|
require "securerandom"
|
|
4
10
|
require "time"
|
|
5
11
|
require "uri"
|
|
6
12
|
require "cgi"
|
|
13
|
+
require_relative "parse_json"
|
|
7
14
|
|
|
8
15
|
module Tina4
|
|
9
16
|
# Middleware wrapper that tags requests arriving on the AI dev port.
|
|
@@ -503,6 +510,9 @@ module Tina4
|
|
|
503
510
|
|
|
504
511
|
# Skip underscore-prefixed segments — private files even within pages/.
|
|
505
512
|
return nil if clean_path.split("/").any? { |seg| seg.start_with?("_") }
|
|
513
|
+
# ADR-0082: a "." / ".." segment (or a backslash) could walk out of
|
|
514
|
+
# pages/ - the only directory that auto-routes. Refuse it outright.
|
|
515
|
+
return nil if clean_path.include?("\\") || clean_path.split("/", -1).any? { |seg| ["", ".", ".."].include?(seg) }
|
|
506
516
|
|
|
507
517
|
is_dev = %w[true 1 yes].include?(ENV.fetch("TINA4_DEBUG", "false").downcase)
|
|
508
518
|
|
|
@@ -1316,7 +1326,7 @@ module Tina4
|
|
|
1316
1326
|
|
|
1317
1327
|
if content_type.include?("application/json")
|
|
1318
1328
|
begin
|
|
1319
|
-
parsed =
|
|
1329
|
+
parsed = Tina4.parse_json(body_str)
|
|
1320
1330
|
return parsed["formToken"] if parsed.is_a?(Hash) && parsed["formToken"]
|
|
1321
1331
|
rescue JSON::ParserError
|
|
1322
1332
|
# Not valid JSON — fall through
|
data/lib/tina4/rate_limiter.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
class RateLimiter
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "fileutils"
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "securerandom"
|
|
4
10
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
module Tina4
|
|
4
10
|
module Realtime
|
data/lib/tina4/realtime.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
|
|
3
9
|
require "json"
|
|
4
10
|
require "time"
|
|
5
11
|
require "openssl"
|
|
6
|
-
|
|
12
|
+
require_relative "base64"
|
|
7
13
|
|
|
8
14
|
require_relative "realtime/workspace"
|
|
9
15
|
require_relative "realtime/channel"
|
|
@@ -14,6 +20,7 @@ require_relative "realtime/storage_backend"
|
|
|
14
20
|
require_relative "realtime/local_storage"
|
|
15
21
|
require_relative "realtime/s3_storage"
|
|
16
22
|
require_relative "realtime/storage"
|
|
23
|
+
require_relative "parse_json"
|
|
17
24
|
|
|
18
25
|
module Tina4
|
|
19
26
|
# Real-time collaboration mount for Tina4 (Ruby), parity with the Python
|
|
@@ -57,7 +64,7 @@ module Tina4
|
|
|
57
64
|
if turn_url && !turn_url.empty? && secret && !secret.empty?
|
|
58
65
|
ttl = (ENV["TINA4_RTC_TURN_TTL"] || "3600").to_i
|
|
59
66
|
username = (Time.now.to_i + ttl).to_s
|
|
60
|
-
credential = Base64.strict_encode64(OpenSSL::HMAC.digest("SHA1", secret, username))
|
|
67
|
+
credential = Tina4::Base64.strict_encode64(OpenSSL::HMAC.digest("SHA1", secret, username))
|
|
61
68
|
servers << { "urls" => split_urls(turn_url), "username" => username, "credential" => credential }
|
|
62
69
|
end
|
|
63
70
|
servers
|
|
@@ -312,7 +319,7 @@ module Tina4
|
|
|
312
319
|
def parse_json(data)
|
|
313
320
|
return data if data.is_a?(Hash)
|
|
314
321
|
|
|
315
|
-
|
|
322
|
+
Tina4.parse_json(data.to_s)
|
|
316
323
|
rescue JSON::ParserError, TypeError
|
|
317
324
|
nil
|
|
318
325
|
end
|
data/lib/tina4/request.rb
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# Copyright (c) 2026 Code Infinity
|
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
|
|
2
8
|
require "uri"
|
|
3
9
|
require "json"
|
|
4
10
|
require "ipaddr"
|
|
5
11
|
require "stringio"
|
|
12
|
+
require_relative "parse_json"
|
|
13
|
+
require_relative "form_parser"
|
|
6
14
|
|
|
7
15
|
module Tina4
|
|
8
16
|
# A Hash subclass that supports indifferent access (both string and symbol keys).
|
|
@@ -122,24 +130,36 @@ module Tina4
|
|
|
122
130
|
# reads route.auth_required to honour a public write route (.no_auth).
|
|
123
131
|
attr_accessor :user, :route
|
|
124
132
|
|
|
125
|
-
#
|
|
126
|
-
TINA4_MAX_UPLOAD_SIZE
|
|
133
|
+
# Default upload cap in bytes (10 MB). The live cap is max_upload_size, which
|
|
134
|
+
# reads TINA4_MAX_UPLOAD_SIZE when it is used (ADR-0072). This constant used
|
|
135
|
+
# to read the environment at require time, which raised on a bad value.
|
|
136
|
+
TINA4_MAX_UPLOAD_SIZE = 10_485_760
|
|
127
137
|
|
|
128
138
|
class PayloadTooLarge < StandardError; end
|
|
129
139
|
|
|
130
140
|
# Effective upload cap in bytes. Read at CALL TIME (not frozen into the
|
|
131
141
|
# constant at load) so the limit honours a TINA4_MAX_UPLOAD_SIZE set after
|
|
132
142
|
# this file was required, and so a test can lower it. Falls back to the
|
|
133
|
-
# constant default when the env var is unset/blank.
|
|
143
|
+
# constant default when the env var is unset/blank. A value that is not a
|
|
144
|
+
# positive whole number warns once and uses the default (ADR-0068): to_i
|
|
145
|
+
# used to turn "abc" into 0, which refused every request with a body.
|
|
134
146
|
def self.max_upload_size
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
raw = ENV["TINA4_MAX_UPLOAD_SIZE"].to_s.strip
|
|
148
|
+
return TINA4_MAX_UPLOAD_SIZE if raw.empty?
|
|
149
|
+
return raw.to_i if raw.match?(/\A\d+\z/) && raw.to_i.positive?
|
|
150
|
+
|
|
151
|
+
@warned_upload_limits ||= {}
|
|
152
|
+
unless @warned_upload_limits[raw]
|
|
153
|
+
@warned_upload_limits[raw] = true
|
|
154
|
+
Tina4::Log.warning("TINA4_MAX_UPLOAD_SIZE=#{raw} is not a usable limit - using #{TINA4_MAX_UPLOAD_SIZE}")
|
|
155
|
+
end
|
|
156
|
+
TINA4_MAX_UPLOAD_SIZE
|
|
137
157
|
end
|
|
138
158
|
|
|
139
159
|
# Read an IO in bounded chunks, raising PayloadTooLarge the MOMENT the
|
|
140
160
|
# running total exceeds the upload cap, so an over-limit body is refused as
|
|
141
161
|
# it arrives instead of after the whole thing is buffered. Rewinds the input
|
|
142
|
-
# before and after so a later reader (
|
|
162
|
+
# before and after so a later reader (the form parser, form-token extraction)
|
|
143
163
|
# sees the same stream. Parity with the Python/Node per-chunk body readers.
|
|
144
164
|
def self.read_stream_capped(input, limit = nil)
|
|
145
165
|
return "" unless input
|
|
@@ -258,7 +278,7 @@ module Tina4
|
|
|
258
278
|
# @return [Boolean] true when the client's scheme is https.
|
|
259
279
|
def self.secure_scheme?(env)
|
|
260
280
|
forwarded = (env["HTTP_X_FORWARDED_PROTO"] || "").to_s
|
|
261
|
-
|
|
281
|
+
if Tina4.trusted_proxy?(env["REMOTE_ADDR"].to_s) && !forwarded.strip.empty?
|
|
262
282
|
return forwarded.split(",").first.to_s.strip.casecmp("https").zero?
|
|
263
283
|
end
|
|
264
284
|
scheme = (env["rack.url_scheme"] || "").to_s
|
|
@@ -272,7 +292,8 @@ module Tina4
|
|
|
272
292
|
# still see the URL the client used. Matches Python/PHP/Node parity.
|
|
273
293
|
def url
|
|
274
294
|
scheme = self.class.secure_scheme?(env) ? "https" : "http"
|
|
275
|
-
|
|
295
|
+
forwarded_host = Tina4.trusted_proxy?(@remote_ip) ? env["HTTP_X_FORWARDED_HOST"] : nil
|
|
296
|
+
host = forwarded_host || env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
|
|
276
297
|
port = env["SERVER_PORT"]
|
|
277
298
|
url_str = "#{scheme}://#{host}"
|
|
278
299
|
# Only append :port when the host doesn't already include one
|
|
@@ -395,7 +416,7 @@ module Tina4
|
|
|
395
416
|
|
|
396
417
|
def json_body
|
|
397
418
|
@json_body ||= begin
|
|
398
|
-
|
|
419
|
+
Tina4.parse_json(body_raw)
|
|
399
420
|
rescue JSON::ParserError, TypeError
|
|
400
421
|
{}
|
|
401
422
|
end
|
|
@@ -474,36 +495,47 @@ module Tina4
|
|
|
474
495
|
data
|
|
475
496
|
end
|
|
476
497
|
|
|
477
|
-
#
|
|
498
|
+
# The parsed multipart body, memoised: { fields:, files:, error: } from
|
|
499
|
+
# Tina4::FormParser, or nil when there is no raw body to parse.
|
|
478
500
|
#
|
|
479
|
-
# A live
|
|
480
|
-
# it
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
501
|
+
# A live server hands the app the raw `rack.input` stream and parses nothing,
|
|
502
|
+
# so Tina4 parses it here. The body is read through read_stream_capped, so the
|
|
503
|
+
# running upload cap refuses an over-limit body as it arrives - OUTSIDE any
|
|
504
|
+
# rescue on purpose: PayloadTooLarge must reach the 413 handler.
|
|
505
|
+
def multipart_parsed
|
|
506
|
+
return @multipart_parsed if defined?(@multipart_parsed)
|
|
507
|
+
|
|
508
|
+
body = Tina4::Request.read_stream_capped(@env["rack.input"])
|
|
509
|
+
@multipart_parsed =
|
|
510
|
+
if body.nil? || body.empty?
|
|
511
|
+
nil
|
|
512
|
+
else
|
|
513
|
+
parsed = Tina4::FormParser.parse_multipart(body, @content_type)
|
|
514
|
+
if parsed[:error] && defined?(Tina4::Log)
|
|
515
|
+
Tina4::Log.warning("multipart parse failed: #{parsed[:error].message}")
|
|
516
|
+
end
|
|
517
|
+
parsed
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# The request's form hash: fields plus file entries under the standard
|
|
522
|
+
# `rack.request.form_hash` env key, the same key (and shape) Rack::Request#POST
|
|
523
|
+
# used to fill, so middleware reading it keeps working. A spec or middleware
|
|
524
|
+
# that INJECTS the key short-circuits the parse entirely.
|
|
487
525
|
def multipart_form_hash
|
|
488
526
|
existing = @env["rack.request.form_hash"] rescue nil
|
|
489
527
|
return existing if existing
|
|
490
528
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
Rack::Request.new(@env).POST
|
|
500
|
-
rescue Tina4::Request::PayloadTooLarge
|
|
501
|
-
raise
|
|
502
|
-
rescue StandardError => e
|
|
503
|
-
Tina4::Log.warning("multipart parse failed: #{e.message}") if defined?(Tina4::Log)
|
|
504
|
-
nil
|
|
529
|
+
parsed = multipart_parsed
|
|
530
|
+
return nil unless parsed
|
|
531
|
+
|
|
532
|
+
form = parsed[:fields].dup
|
|
533
|
+
parsed[:files].each do |name, list|
|
|
534
|
+
# Rack left an empty file field (filename="") out of the form hash.
|
|
535
|
+
kept = list.reject { |descriptor| descriptor[:filename].empty? }
|
|
536
|
+
form[name] = kept.length == 1 ? kept.first : kept unless kept.empty?
|
|
505
537
|
end
|
|
506
|
-
@env["rack.request.form_hash"]
|
|
538
|
+
@env["rack.request.form_hash"] = form
|
|
507
539
|
end
|
|
508
540
|
|
|
509
541
|
def parse_body
|
|
@@ -521,14 +553,14 @@ module Tina4
|
|
|
521
553
|
# NOT delegating to #json_body: that method keeps its own documented
|
|
522
554
|
# always-a-Hash, {}-on-failure contract for direct callers.
|
|
523
555
|
begin
|
|
524
|
-
|
|
556
|
+
Tina4.parse_json(body_raw)
|
|
525
557
|
rescue JSON::ParserError, TypeError
|
|
526
558
|
body_raw
|
|
527
559
|
end
|
|
528
560
|
elsif @content_type.include?("application/x-www-form-urlencoded")
|
|
529
561
|
parse_query_to_hash(body_raw)
|
|
530
562
|
elsif @content_type.include?("multipart/form-data")
|
|
531
|
-
#
|
|
563
|
+
# Form fields from the parsed multipart body (Tina4::FormParser).
|
|
532
564
|
# Files are handled separately by extract_files.
|
|
533
565
|
result = {}
|
|
534
566
|
form_hash = multipart_form_hash
|
|
@@ -560,38 +592,27 @@ module Tina4
|
|
|
560
592
|
def extract_files
|
|
561
593
|
result = {}
|
|
562
594
|
return result unless @content_type.include?("multipart/form-data")
|
|
563
|
-
begin
|
|
564
|
-
# LIVE path: hand-scan the raw body so a REPEATED file field name is
|
|
565
|
-
# preserved as a LIST (Rack's POST collapses a repeated non-bracket name
|
|
566
|
-
# to last-wins - the multi-file data-loss this fixes). The scan reads the
|
|
567
|
-
# body through the capped reader, so an over-limit upload is refused as
|
|
568
|
-
# it arrives. Falls through to the parsed form_hash when there is no raw
|
|
569
|
-
# body (e.g. a spec injecting rack.request.form_hash).
|
|
570
|
-
scanned = scan_multipart_files
|
|
571
|
-
if scanned && !scanned.empty?
|
|
572
|
-
scanned.each do |key, list|
|
|
573
|
-
files = list.map { |value| build_file_upload(value) }.compact
|
|
574
|
-
result[key] = files.length == 1 ? files.first : files unless files.empty?
|
|
575
|
-
end
|
|
576
|
-
return result
|
|
577
|
-
end
|
|
578
595
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
596
|
+
# LIVE path: the parser keeps a REPEATED file field name as a LIST (Rack's
|
|
597
|
+
# POST collapsed a repeated non-bracket name to last-wins - the multi-file
|
|
598
|
+
# data loss). Falls through to an injected rack.request.form_hash when
|
|
599
|
+
# there is no raw body to parse.
|
|
600
|
+
injected = @env["rack.request.form_hash"] unless multipart_parsed
|
|
601
|
+
if multipart_parsed
|
|
602
|
+
multipart_parsed[:files].each do |key, list|
|
|
603
|
+
files = list.map { |value| build_file_upload(value) }.compact
|
|
604
|
+
result[key] = files.length == 1 ? files.first : files unless files.empty?
|
|
605
|
+
end
|
|
606
|
+
elsif injected
|
|
607
|
+
injected.each do |key, value|
|
|
608
|
+
if value.is_a?(Array)
|
|
609
|
+
files = value.map { |v| build_file_upload(v) }.compact
|
|
610
|
+
result[key] = files.length == 1 ? files.first : files unless files.empty?
|
|
611
|
+
else
|
|
612
|
+
file = build_file_upload(value)
|
|
613
|
+
result[key] = file if file
|
|
589
614
|
end
|
|
590
615
|
end
|
|
591
|
-
rescue Tina4::Request::PayloadTooLarge
|
|
592
|
-
raise
|
|
593
|
-
rescue StandardError
|
|
594
|
-
# Multipart parsing failed
|
|
595
616
|
end
|
|
596
617
|
result
|
|
597
618
|
end
|
|
@@ -612,68 +633,5 @@ module Tina4
|
|
|
612
633
|
file["content"] = value[:content] if value.key?(:content) && !value[:content].nil?
|
|
613
634
|
file
|
|
614
635
|
end
|
|
615
|
-
|
|
616
|
-
# Extract the boundary token from a multipart Content-Type header.
|
|
617
|
-
def multipart_boundary(content_type)
|
|
618
|
-
content_type.to_s.split(";").each do |part|
|
|
619
|
-
part = part.strip
|
|
620
|
-
next unless part.start_with?("boundary=")
|
|
621
|
-
|
|
622
|
-
return part[9..].to_s.delete_prefix('"').delete_suffix('"')
|
|
623
|
-
end
|
|
624
|
-
nil
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
# Hand-roll the FILE parts out of the raw multipart body, keyed by field
|
|
628
|
-
# name, each name mapping to a LIST of descriptors so a repeated name keeps
|
|
629
|
-
# every file. Reads through read_stream_capped, so the running per-chunk
|
|
630
|
-
# upload cap is enforced here too (raising PayloadTooLarge). Returns {} when
|
|
631
|
-
# there is no raw body (the injected-form_hash path is used instead). Fields
|
|
632
|
-
# are handled by parse_body (via Rack) - this scans files only.
|
|
633
|
-
def scan_multipart_files
|
|
634
|
-
boundary = multipart_boundary(@content_type)
|
|
635
|
-
return {} unless boundary
|
|
636
|
-
|
|
637
|
-
body = Tina4::Request.read_stream_capped(@env["rack.input"])
|
|
638
|
-
return {} if body.nil? || body.empty?
|
|
639
|
-
|
|
640
|
-
body = body.dup.force_encoding("BINARY")
|
|
641
|
-
files = {}
|
|
642
|
-
delimiter = "--#{boundary}"
|
|
643
|
-
body.split(delimiter).each do |segment|
|
|
644
|
-
next if segment.empty? || segment.start_with?("--")
|
|
645
|
-
|
|
646
|
-
segment = segment.sub(/\A\r\n/, "")
|
|
647
|
-
header_end = segment.index("\r\n\r\n")
|
|
648
|
-
next unless header_end
|
|
649
|
-
|
|
650
|
-
header_section = segment[0...header_end]
|
|
651
|
-
content = segment[(header_end + 4)..] || ""
|
|
652
|
-
content = content.sub(/\r\n\z/, "")
|
|
653
|
-
|
|
654
|
-
name = nil
|
|
655
|
-
filename = nil
|
|
656
|
-
type = "application/octet-stream"
|
|
657
|
-
header_section.split("\r\n").each do |line|
|
|
658
|
-
if line =~ /content-disposition/i
|
|
659
|
-
name = Regexp.last_match(1) if line =~ /name="([^"]*)"/
|
|
660
|
-
filename = Regexp.last_match(1) if line =~ /filename="([^"]*)"/
|
|
661
|
-
elsif line =~ /content-type:\s*(.+)/i
|
|
662
|
-
type = Regexp.last_match(1).strip
|
|
663
|
-
end
|
|
664
|
-
end
|
|
665
|
-
next if name.nil? || filename.nil?
|
|
666
|
-
|
|
667
|
-
bytes = content.dup.force_encoding("BINARY")
|
|
668
|
-
(files[name] ||= []) << {
|
|
669
|
-
filename: filename,
|
|
670
|
-
type: type,
|
|
671
|
-
content: bytes,
|
|
672
|
-
size: bytes.bytesize,
|
|
673
|
-
tempfile: StringIO.new(bytes)
|
|
674
|
-
}
|
|
675
|
-
end
|
|
676
|
-
files
|
|
677
|
-
end
|
|
678
636
|
end
|
|
679
637
|
end
|