mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2025-12-17 14:19:05 +08:00
Merge pull request #2879 from aignatov-bio/ai-sci-4957-update-discover-for-wopi
Refactor WOPI discovery [SCI-4957]
This commit is contained in:
commit
c0102338e0
5 changed files with 79 additions and 90 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
class WopiController < ActionController::Base
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WopiController < ApplicationController
|
||||||
include WopiUtil
|
include WopiUtil
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
@ -334,7 +336,7 @@ class WopiController < ActionController::Base
|
||||||
url = request.original_url.upcase.encode('utf-8')
|
url = request.original_url.upcase.encode('utf-8')
|
||||||
|
|
||||||
if convert_to_unix_timestamp(timestamp) + 20.minutes >= Time.now
|
if convert_to_unix_timestamp(timestamp) + 20.minutes >= Time.now
|
||||||
if current_wopi_discovery.verify_proof(token, timestamp, signed_proof, signed_proof_old, url)
|
if wopi_verify_proof(token, timestamp, signed_proof, signed_proof_old, url)
|
||||||
logger.warn 'WOPI: proof verification: successful'
|
logger.warn 'WOPI: proof verification: successful'
|
||||||
else
|
else
|
||||||
logger.warn 'WOPI: proof verification: not verified'
|
logger.warn 'WOPI: proof verification: not verified'
|
||||||
|
|
|
||||||
|
|
@ -320,7 +320,7 @@ class Asset < ApplicationRecord
|
||||||
file_ext = file_name.split('.').last
|
file_ext = file_name.split('.').last
|
||||||
action = get_action(file_ext, action)
|
action = get_action(file_ext, action)
|
||||||
if !action.nil?
|
if !action.nil?
|
||||||
action_url = action.urlsrc
|
action_url = action[:urlsrc]
|
||||||
if ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS'] == 'true'
|
if ENV['WOPI_BUSINESS_USERS'] && ENV['WOPI_BUSINESS_USERS'] == 'true'
|
||||||
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
|
action_url = action_url.gsub(/<IsLicensedUser=BUSINESS_USER&>/,
|
||||||
'IsLicensedUser=1&')
|
'IsLicensedUser=1&')
|
||||||
|
|
@ -354,7 +354,7 @@ class Asset < ApplicationRecord
|
||||||
def favicon_url(action)
|
def favicon_url(action)
|
||||||
file_ext = file_name.split('.').last
|
file_ext = file_name.split('.').last
|
||||||
action = get_action(file_ext, action)
|
action = get_action(file_ext, action)
|
||||||
action.wopi_app.icon if action.try(:wopi_app)
|
action[:icon] if action[:icon]
|
||||||
end
|
end
|
||||||
|
|
||||||
# locked?, lock_asset and refresh_lock rely on the asset
|
# locked?, lock_asset and refresh_lock rely on the asset
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,4 @@ class WopiAction < ApplicationRecord
|
||||||
belongs_to :wopi_app, foreign_key: 'wopi_app_id', class_name: 'WopiApp'
|
belongs_to :wopi_app, foreign_key: 'wopi_app_id', class_name: 'WopiApp'
|
||||||
|
|
||||||
validates :action, :extension, :urlsrc, :wopi_app, presence: true
|
validates :action, :extension, :urlsrc, :wopi_app, presence: true
|
||||||
|
|
||||||
def self.find_action(extension, activity)
|
|
||||||
WopiAction.distinct
|
|
||||||
.where('extension = ? and action = ?', extension, activity).first
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,43 +13,4 @@ class WopiDiscovery < ApplicationRecord
|
||||||
:proof_key_old_mod,
|
:proof_key_old_mod,
|
||||||
:proof_key_old_exp,
|
:proof_key_old_exp,
|
||||||
presence: true
|
presence: true
|
||||||
|
|
||||||
# Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted
|
|
||||||
# with this discovery public key (two key possible old/new)
|
|
||||||
def verify_proof(token, timestamp, signed_proof, signed_proof_old, url)
|
|
||||||
token_length = [token.length].pack('>N').bytes
|
|
||||||
timestamp_bytes = [timestamp.to_i].pack('>Q').bytes.reverse
|
|
||||||
timestamp_length = [timestamp_bytes.length].pack('>N').bytes
|
|
||||||
url_length = [url.length].pack('>N').bytes
|
|
||||||
|
|
||||||
expected_proof = token_length + token.bytes +
|
|
||||||
url_length + url.upcase.bytes +
|
|
||||||
timestamp_length + timestamp_bytes
|
|
||||||
|
|
||||||
key = generate_key(proof_key_mod, proof_key_exp)
|
|
||||||
old_key = generate_key(proof_key_old_mod, proof_key_old_exp)
|
|
||||||
|
|
||||||
# Try all possible combiniations
|
|
||||||
try_verification(expected_proof, signed_proof, key) ||
|
|
||||||
try_verification(expected_proof, signed_proof_old, key) ||
|
|
||||||
try_verification(expected_proof, signed_proof, old_key)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generates a public key from given modulus and exponent
|
|
||||||
def generate_key(modulus, exponent)
|
|
||||||
mod = Base64.decode64(modulus).unpack('H*').first.to_i(16)
|
|
||||||
exp = Base64.decode64(exponent).unpack('H*').first.to_i(16)
|
|
||||||
|
|
||||||
seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod),
|
|
||||||
OpenSSL::ASN1::Integer.new(exp)])
|
|
||||||
OpenSSL::PKey::RSA.new(seq.to_der)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Verify if decrypting signed_proof with public_key equals to expected_proof
|
|
||||||
def try_verification(expected_proof, signed_proof_b64, public_key)
|
|
||||||
signed_proof = Base64.decode64(signed_proof_b64)
|
|
||||||
public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof,
|
|
||||||
expected_proof.pack('c*'))
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module WopiUtil
|
module WopiUtil
|
||||||
require 'open-uri'
|
require 'open-uri'
|
||||||
|
|
||||||
|
|
@ -5,72 +7,101 @@ module WopiUtil
|
||||||
UNIX_EPOCH_IN_CLR_TICKS = 621355968000000000
|
UNIX_EPOCH_IN_CLR_TICKS = 621355968000000000
|
||||||
CLR_TICKS_PER_SECOND = 10000000
|
CLR_TICKS_PER_SECOND = 10000000
|
||||||
|
|
||||||
DISCOVERY_TTL = 1.days
|
DISCOVERY_TTL = 1.day
|
||||||
DISCOVERY_TTL.freeze
|
DISCOVERY_TTL.freeze
|
||||||
|
|
||||||
# For more explanation see this:
|
# For more explanation see this:
|
||||||
# http://stackoverflow.com/questions/11888053/
|
# http://stackoverflow.com/questions/11888053/
|
||||||
# convert-net-datetime-ticks-property-to-date-in-objective-c
|
# convert-net-datetime-ticks-property-to-date-in-objective-c
|
||||||
def convert_to_unix_timestamp(timestamp)
|
def convert_to_unix_timestamp(timestamp)
|
||||||
Time.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND)
|
Time.zone.at((timestamp - UNIX_EPOCH_IN_CLR_TICKS) / CLR_TICKS_PER_SECOND)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_action(extension, activity)
|
def get_action(extension, action)
|
||||||
current_wopi_discovery
|
discovery = current_wopi_discovery
|
||||||
WopiAction.find_action(extension, activity)
|
discovery[:actions].find { |i| i[:extension] == extension && i[:action] == action }
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_wopi_discovery
|
def current_wopi_discovery
|
||||||
discovery = WopiDiscovery.first
|
initialize_discovery
|
||||||
return discovery if discovery && discovery.expires >= Time.now.to_i
|
end
|
||||||
initialize_discovery(discovery)
|
|
||||||
|
# Verifies if proof from headers, X-WOPI-Proof/X-WOPI-OldProof was encrypted
|
||||||
|
# with this discovery public key (two key possible old/new)
|
||||||
|
def wopi_verify_proof(token, timestamp, signed_proof, signed_proof_old, url)
|
||||||
|
discovery = current_wopi_discovery
|
||||||
|
token_length = [token.length].pack('>N').bytes
|
||||||
|
timestamp_bytes = [timestamp.to_i].pack('>Q').bytes.reverse
|
||||||
|
timestamp_length = [timestamp_bytes.length].pack('>N').bytes
|
||||||
|
url_length = [url.length].pack('>N').bytes
|
||||||
|
|
||||||
|
expected_proof = token_length + token.bytes +
|
||||||
|
url_length + url.upcase.bytes +
|
||||||
|
timestamp_length + timestamp_bytes
|
||||||
|
|
||||||
|
key = generate_key(discovery[:proof_key_mod], discovery[:proof_key_exp])
|
||||||
|
old_key = generate_key(discovery[:proof_key_old_mod], discovery[:proof_key_old_exp])
|
||||||
|
|
||||||
|
# Try all possible combiniations
|
||||||
|
try_verification(expected_proof, signed_proof, key) ||
|
||||||
|
try_verification(expected_proof, signed_proof_old, key) ||
|
||||||
|
try_verification(expected_proof, signed_proof, old_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Currently only saves Excel, Word and PowerPoint view and edit actions
|
# Currently only saves Excel, Word and PowerPoint view and edit actions
|
||||||
def initialize_discovery(discovery)
|
def initialize_discovery
|
||||||
Rails.logger.warn 'Initializing discovery'
|
Rails.cache.fetch(:wopi_discovery, expires_in: DISCOVERY_TTL) do
|
||||||
discovery.destroy if discovery
|
@doc = Nokogiri::XML(Kernel.open(ENV['WOPI_DISCOVERY_URL']))
|
||||||
|
discovery_json = {}
|
||||||
|
key = @doc.xpath('//proof-key')
|
||||||
|
discovery_json[:proof_key_mod] = key.xpath('@modulus').first.value
|
||||||
|
discovery_json[:proof_key_exp] = key.xpath('@exponent').first.value
|
||||||
|
discovery_json[:proof_key_old_mod] = key.xpath('@oldmodulus').first.value
|
||||||
|
discovery_json[:proof_key_old_exp] = key.xpath('@oldexponent').first.value
|
||||||
|
discovery_json[:actions] = []
|
||||||
|
|
||||||
@doc = Nokogiri::XML(Kernel.open(ENV['WOPI_DISCOVERY_URL']))
|
@doc.xpath('//app').each do |app|
|
||||||
|
app_name = app.xpath('@name').first.value
|
||||||
|
next unless %w(Excel Word PowerPoint WopiTest).include?(app_name)
|
||||||
|
|
||||||
discovery = WopiDiscovery.new
|
icon = app.xpath('@favIconUrl').first.value
|
||||||
discovery.expires = Time.now.to_i + DISCOVERY_TTL
|
|
||||||
key = @doc.xpath('//proof-key')
|
|
||||||
discovery.proof_key_mod = key.xpath('@modulus').first.value
|
|
||||||
discovery.proof_key_exp = key.xpath('@exponent').first.value
|
|
||||||
discovery.proof_key_old_mod = key.xpath('@oldmodulus').first.value
|
|
||||||
discovery.proof_key_old_exp = key.xpath('@oldexponent').first.value
|
|
||||||
discovery.save!
|
|
||||||
|
|
||||||
@doc.xpath('//app').each do |app|
|
app.xpath('action').each do |action|
|
||||||
app_name = app.xpath('@name').first.value
|
action_name = action.xpath('@name').first.value
|
||||||
next unless %w(Excel Word PowerPoint WopiTest).include?(app_name)
|
next unless %w(view edit editnew embedview wopitest).include?(action_name)
|
||||||
|
|
||||||
wopi_app = WopiApp.new
|
action_json = {}
|
||||||
wopi_app.name = app.xpath('@name').first.value
|
action_json[:icon] = icon
|
||||||
wopi_app.icon = app.xpath('@favIconUrl').first.value
|
action_json[:action] = action_name
|
||||||
wopi_app.wopi_discovery_id = discovery.id
|
action_json[:extension] = action.xpath('@ext').first.value
|
||||||
wopi_app.save!
|
action_json[:urlsrc] = action.xpath('@urlsrc').first.value
|
||||||
app.xpath('action').each do |action|
|
discovery_json[:actions].push(action_json)
|
||||||
name = action.xpath('@name').first.value
|
end
|
||||||
next unless %w(view edit editnew embedview wopitest).include?(name)
|
|
||||||
|
|
||||||
wopi_action = WopiAction.new
|
|
||||||
wopi_action.action = name
|
|
||||||
wopi_action.extension = action.xpath('@ext').first.value
|
|
||||||
wopi_action.urlsrc = action.xpath('@urlsrc').first.value
|
|
||||||
wopi_action.wopi_app_id = wopi_app.id
|
|
||||||
wopi_action.save!
|
|
||||||
end
|
end
|
||||||
|
discovery_json
|
||||||
end
|
end
|
||||||
discovery
|
rescue StandardError => e
|
||||||
rescue => e
|
|
||||||
Rails.logger.warn 'WOPI: initialization failed: ' + e.message
|
Rails.logger.warn 'WOPI: initialization failed: ' + e.message
|
||||||
e.backtrace.each { |line| Rails.logger.error line }
|
e.backtrace.each { |line| Rails.logger.error line }
|
||||||
discovery = WopiDiscovery.first
|
end
|
||||||
discovery.destroy if discovery
|
|
||||||
|
# Generates a public key from given modulus and exponent
|
||||||
|
def generate_key(modulus, exponent)
|
||||||
|
mod = Base64.decode64(modulus).unpack1('H*').to_i(16)
|
||||||
|
exp = Base64.decode64(exponent).unpack1('H*').to_i(16)
|
||||||
|
|
||||||
|
seq = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(mod),
|
||||||
|
OpenSSL::ASN1::Integer.new(exp)])
|
||||||
|
OpenSSL::PKey::RSA.new(seq.to_der)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Verify if decrypting signed_proof with public_key equals to expected_proof
|
||||||
|
def try_verification(expected_proof, signed_proof_b64, public_key)
|
||||||
|
signed_proof = Base64.decode64(signed_proof_b64)
|
||||||
|
public_key.verify(OpenSSL::Digest::SHA256.new, signed_proof,
|
||||||
|
expected_proof.pack('c*'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_wopi_file_activity(current_user, started_editing)
|
def create_wopi_file_activity(current_user, started_editing)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue