Skip to content

Commit 56336e2

Browse files
authored
Merge branch 'master' into dependabot/bundler/nokogiri-1.16.5
2 parents e454e72 + 2a30c7f commit 56336e2

20 files changed

+460
-294
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Metrics/BlockLength:
131131

132132
# A complexity metric geared towards measuring complexity for a human reader.
133133
Metrics/PerceivedComplexity:
134-
Max: 17
134+
Max: 21
135135
Exclude:
136136
- app/models/recording.rb
137137
- app/models/server.rb

Gemfile.lock

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ GEM
177177
redis-namespace (1.11.0)
178178
redis (>= 4)
179179
regexp_parser (2.7.0)
180-
rexml (3.2.5)
180+
rexml (3.2.8)
181+
strscan (>= 3.0.9)
181182
rspec-core (3.12.1)
182183
rspec-support (~> 3.12.0)
183184
rspec-expectations (3.12.2)
@@ -233,6 +234,7 @@ GEM
233234
sprockets (>= 3.0.0)
234235
sqlite3 (1.6.2)
235236
mini_portile2 (~> 2.8.0)
237+
strscan (3.1.0)
236238
tabulo (2.8.2)
237239
tty-screen (= 0.8.1)
238240
unicode-display_width (~> 2.2)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
class ScaleliteApiController < ApplicationController
5+
include ApiHelper
6+
7+
skip_before_action :verify_authenticity_token
8+
9+
before_action :verify_content_type
10+
before_action -> { verify_checksum(true) }
11+
12+
def verify_content_type
13+
return unless request.post? && request.content_length.positive?
14+
15+
raise UnsupportedContentType unless request.content_mime_type == Mime[:json]
16+
end
17+
end
18+
end

app/controllers/api/servers_controller.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
# frozen_string_literal: true
22

33
module Api
4-
class ServersController < ApplicationController
5-
include ApiHelper
6-
7-
skip_before_action :verify_authenticity_token
8-
9-
before_action -> { verify_checksum(true) }
4+
class ServersController < ScaleliteApiController
105
before_action :set_server, only: [:get_server_info, :update_server, :delete_server, :panic_server]
116

127
# Return a list of the configured BigBlueButton servers

app/controllers/api/tenants_controller.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
# frozen_string_literal: true
22

33
module Api
4-
class TenantsController < ApplicationController
5-
include ApiHelper
6-
7-
skip_before_action :verify_authenticity_token
8-
9-
before_action -> { verify_checksum(true) }
4+
class TenantsController < ScaleliteApiController
105
before_action :check_multitenancy
116
before_action :set_tenant, only: [:get_tenant_info, :update_tenant, :delete_tenant]
127

app/controllers/bigbluebutton_api_controller.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ class BigBlueButtonApiController < ApplicationController
55

66
skip_before_action :verify_authenticity_token
77

8+
# Check content types on endpoints that accept POST requests. For most endpoints, form data is permitted.
9+
before_action :verify_content_type, except: [:create, :insert_document, :join, :publish_recordings, :delete_recordings]
10+
# create allows either form data or XML
11+
before_action :verify_create_content_type, only: [:create]
12+
# insertDocument only allows XML
13+
before_action :verify_insert_document_content_type, only: [:insert_document]
14+
815
before_action :verify_checksum, except: [:index, :get_recordings_disabled, :recordings_disabled, :get_meetings_disabled,
916
:analytics_callback,]
1017

@@ -213,6 +220,13 @@ def create
213220
params[:'meta_secret-lrs-payload'] = lrs_payload if lrs_payload.present?
214221
end
215222

223+
if @tenant&.lrs_endpoint.present?
224+
lrs_payload = LrsPayloadService.new(tenant: @tenant, secret: server.secret).call
225+
params[:'meta_secret-lrs-payload'] = lrs_payload if lrs_payload.present?
226+
end
227+
228+
have_preuploaded_slide = request.post? && request.content_mime_type == Mime[:xml]
229+
216230
logger.debug("Creating meeting #{params[:meetingID]} on BigBlueButton server #{server.id}")
217231
params_hash = params
218232

@@ -224,8 +238,8 @@ def create
224238
uri = encode_bbb_uri('create', server.url, server.secret, pass_through_params(excluded_params))
225239

226240
begin
227-
# Read the body if POST
228-
body = request.post? ? request.body.read : ''
241+
# Read the body if preuploaded slide XML is present
242+
body = have_preuploaded_slide ? request.raw_post : ''
229243

230244
# Send a GET/POST request to the server
231245
response = get_post_req(uri, body, **bbb_req_timeout(server))

app/controllers/concerns/api_helper.rb

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,39 @@ module ApiHelper
1919
# which should be accessed only by superadmins.
2020
def verify_checksum(force_loadbalancer_secret = false)
2121
secrets = fetch_secrets(force_loadbalancer_secret: force_loadbalancer_secret)
22-
23-
raise ChecksumError if params[:checksum].blank?
2422
raise ChecksumError if secrets.empty?
2523

26-
algorithm = case params[:checksum].length
27-
when CHECKSUM_LENGTH_SHA1
28-
'SHA1'
29-
when CHECKSUM_LENGTH_SHA256
30-
'SHA256'
31-
when CHECKSUM_LENGTH_SHA512
32-
'SHA512'
33-
else
34-
raise ChecksumError
35-
end
36-
37-
# Camel case (ex) get_meetings to getMeetings to match BBB server
38-
check_string = action_name.camelcase(:lower)
39-
check_string += request.query_string.gsub(
40-
/&checksum=#{params[:checksum]}|checksum=#{params[:checksum]}&|checksum=#{params[:checksum]}/, ''
41-
)
24+
checksum = request.params[:checksum]
25+
raise ChecksumError if checksum.blank?
4226

27+
algorithm = guess_checksum_digest_algorithm(checksum)
4328
allowed_checksum_algorithms = Rails.configuration.x.loadbalancer_checksum_algorithms
44-
raise ChecksumError unless allowed_checksum_algorithms.include? algorithm
29+
raise ChecksumError unless allowed_checksum_algorithms.include?(algorithm)
4530

46-
secrets.each do |secret|
47-
return true if ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm),
48-
params[:checksum])
31+
check_string = action_name.camelcase(:lower) + query_string_remove_checksum(checksum)
32+
return true if secrets.any? do |secret|
33+
ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm), checksum)
4934
end
5035

5136
raise ChecksumError
5237
end
5338

39+
# Remove the checksum from the request query string. This is done as string manipulation, rather than decoding then re-encoding the parameters,
40+
# since there's multiple possible valid encodings for query parameters, and the one used by Ruby might not match.
41+
def query_string_remove_checksum(checksum)
42+
checksum = Regexp.escape(checksum)
43+
request.query_string.gsub(/&checksum=#{checksum}|checksum=#{checksum}&|checksum=#{checksum}/, '')
44+
end
45+
46+
def guess_checksum_digest_algorithm(checksum)
47+
case checksum.length
48+
when CHECKSUM_LENGTH_SHA1 then 'SHA1'
49+
when CHECKSUM_LENGTH_SHA256 then 'SHA256'
50+
when CHECKSUM_LENGTH_SHA512 then 'SHA512'
51+
else raise ChecksumError
52+
end
53+
end
54+
5455
def fetch_secrets(tenant_name: nil, force_loadbalancer_secret: false)
5556
return Rails.configuration.x.loadbalancer_secrets if force_loadbalancer_secret || !Rails.configuration.x.multitenancy_enabled
5657

@@ -98,6 +99,27 @@ def checksum_algorithm
9899
end
99100
end
100101

102+
# Verify that the Content-Type of POST requests is a "form data" type (applies to most APIs)
103+
def verify_content_type
104+
return unless request.post? && request.content_length.positive?
105+
raise UnsupportedContentType unless request.form_data?
106+
end
107+
108+
# Verify that the Content-Type of a POST request is a format permitted by the create API.
109+
# This can either be form data containing params, or an XML document for pre-uploaded slides
110+
CREATE_PERMITTED_CONTENT_TYPES = Set.new([Mime[:url_encoded_form], Mime[:multipart_form], Mime[:xml]]).freeze
111+
def verify_create_content_type
112+
return unless request.post? && request.content_length.positive?
113+
raise UnsupportedContentType unless CREATE_PERMITTED_CONTENT_TYPES.include?(request.content_mime_type)
114+
end
115+
116+
# Verify that the Content-Type of a POST request is a format permitted by the insertDocument API.
117+
# Only an XML document for pre-uploaded slides (same format as create) is permitted.
118+
def verify_insert_document_content_type
119+
return unless request.post? && request.content_length.positive?
120+
raise UnsupportedContentType unless request.content_mime_type == Mime[:xml]
121+
end
122+
101123
# Encode URI and append checksum
102124
def encode_bbb_uri(action, base_uri, secret, bbb_params = {})
103125
# Add slash at the end if its not there

app/controllers/concerns/bbb_errors.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ def initialize
4040
end
4141
end
4242

43+
class UnsupportedContentType < BBBErrors::BBBError
44+
def initialize
45+
super('unsupportedContentType', 'POST request Content-Type is missing or unsupported')
46+
end
47+
end
48+
4349
class InternalError < BBBError
4450
def initialize(error)
4551
super('internalError', error)

app/models/tenant.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
class Tenant < ApplicationRedisRecord
44
SECRETS_SEPARATOR = ':'
55

6-
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_basic_token, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username,
7-
:kc_password
6+
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_username, :lrs_password,
7+
:kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password
88

99
# Unique ID for this tenant
1010
application_redis_attr :id
@@ -17,7 +17,8 @@ class Tenant < ApplicationRedisRecord
1717

1818
# Custom LRS work
1919
application_redis_attr :lrs_endpoint
20-
application_redis_attr :lrs_basic_token
20+
application_redis_attr :lrs_username
21+
application_redis_attr :lrs_password
2122
application_redis_attr :kc_token_url
2223
application_redis_attr :kc_client_id
2324
application_redis_attr :kc_client_secret
@@ -45,7 +46,8 @@ def save!
4546
pipeline.hset(id_key, 'name', name) if name_changed?
4647
pipeline.hset(id_key, 'secrets', secrets) if secrets_changed?
4748
pipeline.hset(id_key, 'lrs_endpoint', lrs_endpoint) if lrs_endpoint_changed?
48-
pipeline.hset(id_key, 'lrs_basic_token', lrs_basic_token) if lrs_basic_token_changed?
49+
pipeline.hset(id_key, 'lrs_username', lrs_username) if lrs_username_changed?
50+
pipeline.hset(id_key, 'lrs_password', lrs_password) if lrs_password_changed?
4951
pipeline.hset(id_key, 'kc_token_url', kc_token_url) if kc_token_url_changed?
5052
pipeline.hset(id_key, 'kc_client_id', kc_client_id) if kc_client_id_changed?
5153
pipeline.hset(id_key, 'kc_client_secret', kc_client_secret) if kc_client_secret_changed?

app/services/lrs_payload_service.rb

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,24 @@ def initialize(tenant:, secret:)
77
end
88

99
def call
10-
token = @tenant.kc_token_url.present? ? fetch_token_from_keycloak : @tenant.lrs_basic_token
11-
12-
if token.nil?
13-
Rails.logger.warn("LRS Token not found")
14-
return nil
15-
end
16-
1710
lrs_payload = {
1811
lrs_endpoint: @tenant.lrs_endpoint,
19-
lrs_token: token
2012
}
2113

14+
if @tenant.lrs_username.present?
15+
lrs_payload[:lrs_username] = @tenant.lrs_username
16+
lrs_payload[:lrs_password] = @tenant.lrs_password
17+
else
18+
token = fetch_token_from_keycloak
19+
20+
if token.nil?
21+
Rails.logger.warn("LRS Token not found")
22+
return nil
23+
end
24+
25+
lrs_payload[:lrs_token] = token
26+
end
27+
2228
# Generate a random salt
2329
salt = SecureRandom.random_bytes(8)
2430

config/routes.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
Rails.application.routes.draw do
44
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
55

6-
scope 'bigbluebutton/api', as: 'bigbluebutton_api', format: false, defaults: { format: 'xml' } do
6+
scope 'bigbluebutton/api', as: :bigbluebutton_api, format: false, defaults: { format: 'xml' } do
7+
# See https://github.com/bigbluebutton/bigbluebutton/blob/main/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy
8+
# for the definitions of the routes in BigBlueButton itself. Note that both private (BBB internal) and public APIs are in that file.
9+
710
match '/', to: 'bigbluebutton_api#index', via: [:get, :post]
811
match 'isMeetingRunning', to: 'bigbluebutton_api#is_meeting_running', as: :is_meeting_running, via: [:get, :post]
912
match 'getMeetingInfo', to: 'bigbluebutton_api#get_meeting_info', as: :get_meeting_info, via: [:get, :post]
@@ -14,19 +17,19 @@
1417
end
1518
match 'create', to: 'bigbluebutton_api#create', via: [:get, :post]
1619
match 'end', to: 'bigbluebutton_api#end', via: [:get, :post]
17-
match 'join', to: 'bigbluebutton_api#join', via: [:get, :post]
18-
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback', as: :analytics_callback
19-
post 'insertDocument', to: 'bigbluebutton_api#insert_document'
20+
get 'join', to: 'bigbluebutton_api#join'
21+
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback'
22+
post 'insertDocument', to: 'bigbluebutton_api#insert_document', as: :insert_document
2023
if Rails.configuration.x.recording_disabled
2124
match('getRecordings', to: 'bigbluebutton_api#get_recordings_disabled', as: :get_recordings, via: [:get, :post])
22-
match('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings, via: [:get, :post])
25+
get('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings)
2326
match('updateRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :update_recordings, via: [:get, :post])
24-
match('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings, via: [:get, :post])
27+
get('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings)
2528
else
2629
match('getRecordings', to: 'bigbluebutton_api#get_recordings', as: :get_recordings, via: [:get, :post])
27-
match('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings, via: [:get, :post])
30+
get('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings)
2831
match('updateRecordings', to: 'bigbluebutton_api#update_recordings', as: :update_recordings, via: [:get, :post])
29-
match('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings, via: [:get, :post])
32+
get('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings)
3033
end
3134
end
3235

0 commit comments

Comments
 (0)