Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/v1.5-stable'
Browse files Browse the repository at this point in the history
  • Loading branch information
farhatahmad committed Jun 27, 2024
2 parents 4a57c23 + b3aa5e6 commit 538df51
Show file tree
Hide file tree
Showing 18 changed files with 453 additions and 289 deletions.
18 changes: 18 additions & 0 deletions app/controllers/api/scalelite_api_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Api
class ScaleliteApiController < ApplicationController
include ApiHelper

skip_before_action :verify_authenticity_token

before_action :verify_content_type
before_action -> { verify_checksum(true) }

def verify_content_type
return unless request.post? && request.content_length.positive?

raise UnsupportedContentType unless request.content_mime_type == Mime[:json]
end
end
end
7 changes: 1 addition & 6 deletions app/controllers/api/servers_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# frozen_string_literal: true

module Api
class ServersController < ApplicationController
include ApiHelper

skip_before_action :verify_authenticity_token

before_action -> { verify_checksum(true) }
class ServersController < ScaleliteApiController
before_action :set_server, only: [:get_server_info, :update_server, :delete_server, :panic_server]

# Return a list of the configured BigBlueButton servers
Expand Down
7 changes: 1 addition & 6 deletions app/controllers/api/tenants_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
# frozen_string_literal: true

module Api
class TenantsController < ApplicationController
include ApiHelper

skip_before_action :verify_authenticity_token

before_action -> { verify_checksum(true) }
class TenantsController < ScaleliteApiController
before_action :check_multitenancy
before_action :set_tenant, only: [:get_tenant_info, :update_tenant, :delete_tenant]

Expand Down
18 changes: 16 additions & 2 deletions app/controllers/bigbluebutton_api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ class BigBlueButtonApiController < ApplicationController

skip_before_action :verify_authenticity_token

# Check content types on endpoints that accept POST requests. For most endpoints, form data is permitted.
before_action :verify_content_type, except: [:create, :insert_document, :join, :publish_recordings, :delete_recordings]
# create allows either form data or XML
before_action :verify_create_content_type, only: [:create]
# insertDocument only allows XML
before_action :verify_insert_document_content_type, only: [:insert_document]

before_action :verify_checksum, except: [:index, :get_recordings_disabled, :recordings_disabled, :get_meetings_disabled,
:analytics_callback,]

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

if @tenant&.lrs_endpoint.present?
lrs_payload = LrsPayloadService.new(tenant: @tenant, secret: server.secret).call
params[:'meta_secret-lrs-payload'] = lrs_payload if lrs_payload.present?
end

have_preuploaded_slide = request.post? && request.content_mime_type == Mime[:xml]

logger.debug("Creating meeting #{params[:meetingID]} on BigBlueButton server #{server.id}")
params_hash = params

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

begin
# Read the body if POST
body = request.post? ? request.body.read : ''
# Read the body if preuploaded slide XML is present
body = have_preuploaded_slide ? request.raw_post : ''

# Send a GET/POST request to the server
response = get_post_req(uri, body, **bbb_req_timeout(server))
Expand Down
66 changes: 44 additions & 22 deletions app/controllers/concerns/api_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,39 @@ module ApiHelper
# which should be accessed only by superadmins.
def verify_checksum(force_loadbalancer_secret = false)
secrets = fetch_secrets(force_loadbalancer_secret: force_loadbalancer_secret)

raise ChecksumError if params[:checksum].blank?
raise ChecksumError if secrets.empty?

algorithm = case params[:checksum].length
when CHECKSUM_LENGTH_SHA1
'SHA1'
when CHECKSUM_LENGTH_SHA256
'SHA256'
when CHECKSUM_LENGTH_SHA512
'SHA512'
else
raise ChecksumError
end

# Camel case (ex) get_meetings to getMeetings to match BBB server
check_string = action_name.camelcase(:lower)
check_string += request.query_string.gsub(
/&checksum=#{params[:checksum]}|checksum=#{params[:checksum]}&|checksum=#{params[:checksum]}/, ''
)
checksum = request.params[:checksum]
raise ChecksumError if checksum.blank?

algorithm = guess_checksum_digest_algorithm(checksum)
allowed_checksum_algorithms = Rails.configuration.x.loadbalancer_checksum_algorithms
raise ChecksumError unless allowed_checksum_algorithms.include? algorithm
raise ChecksumError unless allowed_checksum_algorithms.include?(algorithm)

secrets.each do |secret|
return true if ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm),
params[:checksum])
check_string = action_name.camelcase(:lower) + query_string_remove_checksum(checksum)
return true if secrets.any? do |secret|
ActiveSupport::SecurityUtils.secure_compare(get_checksum(check_string + secret, algorithm), checksum)
end

raise ChecksumError
end

# Remove the checksum from the request query string. This is done as string manipulation, rather than decoding then re-encoding the parameters,
# since there's multiple possible valid encodings for query parameters, and the one used by Ruby might not match.
def query_string_remove_checksum(checksum)
checksum = Regexp.escape(checksum)
request.query_string.gsub(/&checksum=#{checksum}|checksum=#{checksum}&|checksum=#{checksum}/, '')
end

def guess_checksum_digest_algorithm(checksum)
case checksum.length
when CHECKSUM_LENGTH_SHA1 then 'SHA1'
when CHECKSUM_LENGTH_SHA256 then 'SHA256'
when CHECKSUM_LENGTH_SHA512 then 'SHA512'
else raise ChecksumError
end
end

def fetch_secrets(tenant_name: nil, force_loadbalancer_secret: false)
return Rails.configuration.x.loadbalancer_secrets if force_loadbalancer_secret || !Rails.configuration.x.multitenancy_enabled

Expand Down Expand Up @@ -98,6 +99,27 @@ def checksum_algorithm
end
end

# Verify that the Content-Type of POST requests is a "form data" type (applies to most APIs)
def verify_content_type
return unless request.post? && request.content_length.positive?
raise UnsupportedContentType unless request.form_data?
end

# Verify that the Content-Type of a POST request is a format permitted by the create API.
# This can either be form data containing params, or an XML document for pre-uploaded slides
CREATE_PERMITTED_CONTENT_TYPES = Set.new([Mime[:url_encoded_form], Mime[:multipart_form], Mime[:xml]]).freeze
def verify_create_content_type
return unless request.post? && request.content_length.positive?
raise UnsupportedContentType unless CREATE_PERMITTED_CONTENT_TYPES.include?(request.content_mime_type)
end

# Verify that the Content-Type of a POST request is a format permitted by the insertDocument API.
# Only an XML document for pre-uploaded slides (same format as create) is permitted.
def verify_insert_document_content_type
return unless request.post? && request.content_length.positive?
raise UnsupportedContentType unless request.content_mime_type == Mime[:xml]
end

# Encode URI and append checksum
def encode_bbb_uri(action, base_uri, secret, bbb_params = {})
# Add slash at the end if its not there
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/concerns/bbb_errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ def initialize
end
end

class UnsupportedContentType < BBBErrors::BBBError
def initialize
super('unsupportedContentType', 'POST request Content-Type is missing or unsupported')
end
end

class InternalError < BBBError
def initialize(error)
super('internalError', error)
Expand Down
10 changes: 6 additions & 4 deletions app/models/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
class Tenant < ApplicationRedisRecord
SECRETS_SEPARATOR = ':'

define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_basic_token, :kc_token_url, :kc_client_id, :kc_client_secret, :kc_username,
:kc_password
define_attribute_methods :id, :name, :secrets, :lrs_endpoint, :lrs_username, :lrs_password,
:kc_token_url, :kc_client_id, :kc_client_secret, :kc_username, :kc_password

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

# Custom LRS work
application_redis_attr :lrs_endpoint
application_redis_attr :lrs_basic_token
application_redis_attr :lrs_username
application_redis_attr :lrs_password
application_redis_attr :kc_token_url
application_redis_attr :kc_client_id
application_redis_attr :kc_client_secret
Expand Down Expand Up @@ -45,7 +46,8 @@ def save!
pipeline.hset(id_key, 'name', name) if name_changed?
pipeline.hset(id_key, 'secrets', secrets) if secrets_changed?
pipeline.hset(id_key, 'lrs_endpoint', lrs_endpoint) if lrs_endpoint_changed?
pipeline.hset(id_key, 'lrs_basic_token', lrs_basic_token) if lrs_basic_token_changed?
pipeline.hset(id_key, 'lrs_username', lrs_username) if lrs_username_changed?
pipeline.hset(id_key, 'lrs_password', lrs_password) if lrs_password_changed?
pipeline.hset(id_key, 'kc_token_url', kc_token_url) if kc_token_url_changed?
pipeline.hset(id_key, 'kc_client_id', kc_client_id) if kc_client_id_changed?
pipeline.hset(id_key, 'kc_client_secret', kc_client_secret) if kc_client_secret_changed?
Expand Down
22 changes: 14 additions & 8 deletions app/services/lrs_payload_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ def initialize(tenant:, secret:)
end

def call
token = @tenant.kc_token_url.present? ? fetch_token_from_keycloak : @tenant.lrs_basic_token

if token.nil?
Rails.logger.warn("LRS Token not found")
return nil
end

lrs_payload = {
lrs_endpoint: @tenant.lrs_endpoint,
lrs_token: token
}

if @tenant.lrs_username.present?
lrs_payload[:lrs_username] = @tenant.lrs_username
lrs_payload[:lrs_password] = @tenant.lrs_password
else
token = fetch_token_from_keycloak

if token.nil?
Rails.logger.warn("LRS Token not found")
return nil
end

lrs_payload[:lrs_token] = token
end

# Generate a random salt
salt = SecureRandom.random_bytes(8)

Expand Down
19 changes: 11 additions & 8 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html

scope 'bigbluebutton/api', as: 'bigbluebutton_api', format: false, defaults: { format: 'xml' } do
scope 'bigbluebutton/api', as: :bigbluebutton_api, format: false, defaults: { format: 'xml' } do
# See https://github.com/bigbluebutton/bigbluebutton/blob/main/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/UrlMappings.groovy
# for the definitions of the routes in BigBlueButton itself. Note that both private (BBB internal) and public APIs are in that file.

match '/', to: 'bigbluebutton_api#index', via: [:get, :post]
match 'isMeetingRunning', to: 'bigbluebutton_api#is_meeting_running', as: :is_meeting_running, via: [:get, :post]
match 'getMeetingInfo', to: 'bigbluebutton_api#get_meeting_info', as: :get_meeting_info, via: [:get, :post]
Expand All @@ -14,19 +17,19 @@
end
match 'create', to: 'bigbluebutton_api#create', via: [:get, :post]
match 'end', to: 'bigbluebutton_api#end', via: [:get, :post]
match 'join', to: 'bigbluebutton_api#join', via: [:get, :post]
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback', as: :analytics_callback
post 'insertDocument', to: 'bigbluebutton_api#insert_document'
get 'join', to: 'bigbluebutton_api#join'
post 'analytics_callback', to: 'bigbluebutton_api#analytics_callback'
post 'insertDocument', to: 'bigbluebutton_api#insert_document', as: :insert_document
if Rails.configuration.x.recording_disabled
match('getRecordings', to: 'bigbluebutton_api#get_recordings_disabled', as: :get_recordings, via: [:get, :post])
match('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings, via: [:get, :post])
get('publishRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :publish_recordings)
match('updateRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :update_recordings, via: [:get, :post])
match('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings, via: [:get, :post])
get('deleteRecordings', to: 'bigbluebutton_api#recordings_disabled', as: :delete_recordings)
else
match('getRecordings', to: 'bigbluebutton_api#get_recordings', as: :get_recordings, via: [:get, :post])
match('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings, via: [:get, :post])
get('publishRecordings', to: 'bigbluebutton_api#publish_recordings', as: :publish_recordings)
match('updateRecordings', to: 'bigbluebutton_api#update_recordings', as: :update_recordings, via: [:get, :post])
match('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings, via: [:get, :post])
get('deleteRecordings', to: 'bigbluebutton_api#delete_recordings', as: :delete_recordings)
end
end

Expand Down
Loading

0 comments on commit 538df51

Please sign in to comment.