Merge pull request #1337 from realm/mk/ssl-verify-callback-2
The sync config object gets a new property called ssl_verify_callback.
This commit is contained in:
commit
590f5845af
|
@ -1,3 +1,10 @@
|
|||
NEXT RELEASE
|
||||
=============================================================
|
||||
|
||||
### Enhancements
|
||||
* Add a callback function used to verify SSL certificates in the sync config.
|
||||
|
||||
|
||||
2.0.0-rc10 Release notes (2017-9-19)
|
||||
=============================================================
|
||||
### Breaking changes
|
||||
|
|
|
@ -92,17 +92,17 @@ class Realm {
|
|||
constructor(config) {}
|
||||
|
||||
/**
|
||||
* Open a realm asynchronously with a promise. If the realm is synced, it will be fully
|
||||
* Open a Realm asynchronously with a promise. If the Realm is synced, it will be fully
|
||||
* synchronized before it is available.
|
||||
* @param {Realm~Configuration} config
|
||||
* @param {Realm~Configuration} config
|
||||
* @returns {ProgressPromise} - a promise that will be resolved with the realm instance when it's available.
|
||||
*/
|
||||
static open(config) {}
|
||||
|
||||
/**
|
||||
* Open a realm asynchronously with a callback. If the realm is synced, it will be fully
|
||||
* Open a Realm asynchronously with a callback. If the Realm is synced, it will be fully
|
||||
* synchronized before it is available.
|
||||
* @param {Realm~Configuration} config
|
||||
* @param {Realm~Configuration} config
|
||||
* @param {callback(error, realm)} - will be called when the realm is ready.
|
||||
* @param {callback(transferred, transferable)} [progressCallback] - an optional callback for download progress notifications
|
||||
* @throws {Error} If anything in the provided `config` is invalid.
|
||||
|
@ -217,7 +217,7 @@ class Realm {
|
|||
/*
|
||||
* Replaces all string columns in this Realm with a string enumeration column and compacts the
|
||||
* database file.
|
||||
*
|
||||
*
|
||||
* Cannot be called from a write transaction.
|
||||
*
|
||||
* Compaction will not occur if other `Realm` instances exist.
|
||||
|
@ -269,12 +269,12 @@ Realm.defaultPath;
|
|||
* This function takes two arguments:
|
||||
* - `oldRealm` - The Realm before migration is performed.
|
||||
* - `newRealm` - The Realm that uses the latest `schema`, which should be modified as necessary.
|
||||
* @property {callback(number, number)} [shouldCompactOnLaunch] - The function called when opening
|
||||
* a Realm for the first time during the life of a process to determine if it should be compacted
|
||||
* @property {callback(number, number)} [shouldCompactOnLaunch] - The function called when opening
|
||||
* a Realm for the first time during the life of a process to determine if it should be compacted
|
||||
* before being returned to the user. The function takes two arguments:
|
||||
* - `totalSize` - The total file size (data + free space)
|
||||
* - `totalSize` - The total file size (data + free space)
|
||||
* - `unusedSize` - The total bytes used by data in the file.
|
||||
* It returns `true` to indicate that an attempt to compact the file should be made. The compaction
|
||||
* It returns `true` to indicate that an attempt to compact the file should be made. The compaction
|
||||
* will be skipped if another process is accessing it.
|
||||
* @property {string} [path={@link Realm.defaultPath}] - The path to the file where the
|
||||
* Realm database should be stored.
|
||||
|
@ -288,14 +288,49 @@ Realm.defaultPath;
|
|||
* object types in this Realm. **Required** when first creating a Realm at this `path`.
|
||||
* @property {number} [schemaVersion] - **Required** (and must be incremented) after
|
||||
* changing the `schema`.
|
||||
* @property {Object} [sync] - Sync configuration parameters with the following
|
||||
* @property {Object} [sync] - Sync configuration parameters with the following
|
||||
* child properties:
|
||||
* - `user` - A `User` object obtained by calling `Realm.Sync.User.login`
|
||||
* - `url` - A `string` which contains a valid Realm Sync url
|
||||
* - `error` - A callback function which is called in error situations
|
||||
* - `error` - A callback function which is called in error situations.
|
||||
* The `error` callback can take up to four optional arguments: `message`, `isFatal`,
|
||||
* `category`, and `code`.
|
||||
* - `validate_ssl` - Indicating if SSL certificates must be validated
|
||||
* - `ssl_trust_certificate_path` - A path where to find trusted SSL certificates
|
||||
* The `error` callback can take up to four optional arguments: `message`, `isFatal`, `category`, and `code`.
|
||||
* - `open_ssl_verify_callback` - A callback function used to accept or reject the server's
|
||||
* SSL certificate. open_ssl_verify_callback is called with an object of type
|
||||
* <code>
|
||||
* {
|
||||
* serverAddress: String,
|
||||
* serverPort: Number,
|
||||
* pemCertificate: String,
|
||||
* acceptedByOpenSSL: Boolean,
|
||||
* depth: Number
|
||||
* }
|
||||
* </code>
|
||||
* The return value of open_ssl_verify_callback decides whether the certificate is accepted (true)
|
||||
* or rejected (false). The open_ssl_verify_callback function is only respected on platforms where
|
||||
* OpenSSL is used for the sync client, e.g. Linux. The open_ssl_verify_callback function is not
|
||||
* allowed to throw exceptions. If the operations needed to verify the certificate lead to an exception,
|
||||
* the exception must be caught explicitly before returning. The return value would typically be false
|
||||
* in case of an exception.
|
||||
*
|
||||
* When the sync client has received the server's certificate chain, it presents every certificate in
|
||||
* the chain to the open_ssl_verify_callback function. The depth argument specifies the position of the
|
||||
* certificate in the chain. depth = 0 represents the actual server certificate. The root
|
||||
* certificate has the highest depth. The certificate of highest depth will be presented first.
|
||||
*
|
||||
* acceptedByOpenSSL is true if OpenSSL has accepted the certificate, and false if OpenSSL has rejected it.
|
||||
* It is generally safe to return true when acceptedByOpenSSL is true. If acceptedByOpenSSL is false, an
|
||||
* independent verification should be made.
|
||||
*
|
||||
* One possible way of using the open_ssl_verify_callback function is to embed the known server certificate
|
||||
* in the client and accept the presented certificate if and only if it is equal to the known certificate.
|
||||
*
|
||||
* The purpose of open_ssl_verify_callback is to enable custom certificate handling and to solve cases where
|
||||
* OpenSSL erroneously rejects valid certificates possibly because OpenSSL doesn't have access to the
|
||||
* proper trust certificates.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -329,7 +364,7 @@ Realm.defaultPath;
|
|||
* otherwise specified.
|
||||
* @property {boolean} [optional] - Signals if this property may be assigned `null` or `undefined`.
|
||||
* @property {boolean} [indexed] - Signals if this property should be indexed. Only supported for
|
||||
* `"string"`, `"int"`, and `"bool"` properties.
|
||||
* `"string"`, `"int"`, and `"bool"` properties.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -265,25 +265,25 @@ declare namespace Realm.Sync {
|
|||
readonly server: string;
|
||||
readonly token: string;
|
||||
static adminUser(adminToken: string, server?: string): User;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated, to be removed in future versions
|
||||
*/
|
||||
static login(server: string, username: string, password: string, callback: (error: any, user: User) => void): void;
|
||||
static login(server: string, username: string, password: string): Promise<Realm.Sync.User>;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated, to be removed in future versions
|
||||
*/
|
||||
*/
|
||||
static register(server: string, username: string, password: string, callback: (error: any, user: User) => void): void;
|
||||
static register(server: string, username: string, password: string): Promise<Realm.Sync.User>;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated, to be removed in versions
|
||||
*/
|
||||
*/
|
||||
static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }, callback: (error: Error | null, user: User | null) => void): void;
|
||||
static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }): Promise<Realm.Sync.User>;
|
||||
|
||||
|
||||
logout(): void;
|
||||
openManagementRealm(): Realm;
|
||||
retrieveAccount(provider: string, username: string): Promise<Account>;
|
||||
|
@ -299,7 +299,7 @@ declare namespace Realm.Sync {
|
|||
userId: string |
|
||||
{ metadataKey: string, metadataValue: string }
|
||||
};
|
||||
|
||||
|
||||
type AccessLevel = 'none' | 'read' | 'write' | 'admin';
|
||||
|
||||
class Permission {
|
||||
|
@ -310,7 +310,7 @@ declare namespace Realm.Sync {
|
|||
readonly mayRead?: boolean;
|
||||
readonly mayWrite?: boolean;
|
||||
readonly mayManage?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionChange {
|
||||
id: string;
|
||||
|
@ -342,19 +342,21 @@ declare namespace Realm.Sync {
|
|||
}
|
||||
|
||||
type ErrorCallback = (message?: string, isFatal?: boolean, category?: string, code?: number) => void;
|
||||
type SSLVerifyCallback = (serverAddress: string, serverPort: number, pemCertificate: string, preverifyOk: number, depth: number) => boolean;
|
||||
|
||||
interface SyncConfiguration {
|
||||
user: User;
|
||||
url: string;
|
||||
validate_ssl?: boolean;
|
||||
ssl_trust_certificate_path?: string;
|
||||
ssl_verify_callback?: SSLVerifyCallback;
|
||||
error?: ErrorCallback;
|
||||
}
|
||||
|
||||
type ProgressNotificationCallback = (transferred: number, transferable: number) => void;
|
||||
type ProgressDirection = 'download' | 'upload';
|
||||
type ProgressMode = 'reportIndefinitely' | 'forCurrentlyOutstandingWork';
|
||||
|
||||
|
||||
/**
|
||||
* Session
|
||||
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Session.html }
|
||||
|
@ -394,7 +396,7 @@ declare namespace Realm.Sync {
|
|||
function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): void;
|
||||
function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void;
|
||||
function setFeatureToken(token: string): void;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated, to be removed in 2.0
|
||||
*/
|
||||
|
@ -458,19 +460,19 @@ declare class Realm {
|
|||
*/
|
||||
static schemaVersion(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): number;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Open a realm asynchronously with a promise. If the realm is synced, it will be fully synchronized before it is available.
|
||||
* @param {Configuration} config
|
||||
* @param {Configuration} config
|
||||
*/
|
||||
static open(config: Realm.Configuration): ProgressPromise;
|
||||
/**
|
||||
* @deprecated in favor of `Realm.open`
|
||||
* Open a realm asynchronously with a callback. If the realm is synced, it will be fully synchronized before it is available.
|
||||
* @param {Configuration} config
|
||||
* @param {Configuration} config
|
||||
* @param {Function} callback will be called when the realm is ready.
|
||||
* @param {ProgressNotificationCallback} progressCallback? a progress notification callback for 'download' direction and 'forCurrentlyOutstandingWork' mode
|
||||
* @param {ProgressNotificationCallback} progressCallback? a progress notification callback for 'download' direction and 'forCurrentlyOutstandingWork' mode
|
||||
*/
|
||||
static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void, progressCallback?: Realm.Sync.ProgressNotificationCallback): void
|
||||
|
||||
|
|
117
src/js_sync.hpp
117
src/js_sync.hpp
|
@ -22,6 +22,8 @@
|
|||
#include <map>
|
||||
#include <set>
|
||||
#include <regex>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
#include "event_loop_dispatcher.hpp"
|
||||
#include "platform.hpp"
|
||||
|
@ -240,6 +242,98 @@ private:
|
|||
const Protected<typename T::Function> m_func;
|
||||
};
|
||||
|
||||
|
||||
// An object of type SSLVerifyCallbackSyncThreadFunctor is registered with the sync client in order
|
||||
// to verify SSL certificates. The SSLVerifyCallbackSyncThreadFunctor object's operator() is called
|
||||
// on the sync client's event loop thread.
|
||||
template <typename T>
|
||||
class SSLVerifyCallbackSyncThreadFunctor {
|
||||
public:
|
||||
SSLVerifyCallbackSyncThreadFunctor(typename T::Context ctx, typename T::Function ssl_verify_func)
|
||||
: m_ctx(Context<T>::get_global_context(ctx))
|
||||
, m_func(ctx, ssl_verify_func)
|
||||
, m_event_loop_dispatcher {SSLVerifyCallbackSyncThreadFunctor<T>::main_loop_handler}
|
||||
, m_mutex{new std::mutex}
|
||||
, m_cond_var{new std::condition_variable}
|
||||
{
|
||||
}
|
||||
|
||||
// This function is called on the sync client's event loop thread.
|
||||
bool operator ()(const std::string& server_address, sync::Session::port_type server_port, const char* pem_data, size_t pem_size, int preverify_ok, int depth)
|
||||
{
|
||||
const std::string pem_certificate {pem_data, pem_size};
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock {*m_mutex};
|
||||
m_ssl_certificate_callback_done = false;
|
||||
}
|
||||
|
||||
// Dispatch the call to the main_loop_handler on the node.js thread.
|
||||
m_event_loop_dispatcher(this, server_address, server_port, pem_certificate, preverify_ok, depth);
|
||||
|
||||
bool ssl_certificate_accepted = false;
|
||||
{
|
||||
// Wait for the return value of the callback function on the node.js main thread.
|
||||
// The sync client blocks during this wait.
|
||||
std::unique_lock<std::mutex> lock(*m_mutex);
|
||||
m_cond_var->wait(lock, [this] { return this->m_ssl_certificate_callback_done; });
|
||||
ssl_certificate_accepted = m_ssl_certificate_accepted;
|
||||
}
|
||||
|
||||
return ssl_certificate_accepted;
|
||||
}
|
||||
|
||||
// main_loop_handler is called on the node.js main thread.
|
||||
// main_loop_handler calls the user callback (m_func) and sends the return value
|
||||
// back to the sync client's event loop thread through a condition variable.
|
||||
static void main_loop_handler(SSLVerifyCallbackSyncThreadFunctor<T>* this_object,
|
||||
const std::string& server_address,
|
||||
sync::Session::port_type server_port,
|
||||
const std::string& pem_certificate,
|
||||
int preverify_ok,
|
||||
int depth)
|
||||
{
|
||||
HANDLESCOPE
|
||||
|
||||
const Protected<typename T::GlobalContext>& ctx = this_object->m_ctx;
|
||||
|
||||
typename T::Object ssl_certificate_object = Object<T>::create_empty(ctx);
|
||||
Object<T>::set_property(ctx, ssl_certificate_object, "serverAddress", Value<T>::from_string(ctx, server_address));
|
||||
Object<T>::set_property(ctx, ssl_certificate_object, "serverPort", Value<T>::from_number(ctx, double(server_port)));
|
||||
Object<T>::set_property(ctx, ssl_certificate_object, "pemCertificate", Value<T>::from_string(ctx, pem_certificate));
|
||||
Object<T>::set_property(ctx, ssl_certificate_object, "acceptedByOpenSSL", Value<T>::from_boolean(ctx, preverify_ok != 0));
|
||||
Object<T>::set_property(ctx, ssl_certificate_object, "depth", Value<T>::from_number(ctx, double(depth)));
|
||||
|
||||
const int argc = 1;
|
||||
typename T::Value arguments[argc] = { ssl_certificate_object };
|
||||
typename T::Value ret_val = Function<T>::callback(ctx, this_object->m_func, typename T::Object(), 1, arguments);
|
||||
bool ret_val_bool = Value<T>::to_boolean(ctx, ret_val);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock {*this_object->m_mutex};
|
||||
this_object->m_ssl_certificate_callback_done = true;
|
||||
this_object->m_ssl_certificate_accepted = ret_val_bool;
|
||||
}
|
||||
|
||||
this_object->m_cond_var->notify_one();
|
||||
};
|
||||
|
||||
|
||||
private:
|
||||
const Protected<typename T::GlobalContext> m_ctx;
|
||||
const Protected<typename T::Function> m_func;
|
||||
EventLoopDispatcher<void(SSLVerifyCallbackSyncThreadFunctor<T>* this_object,
|
||||
const std::string& server_address,
|
||||
sync::Session::port_type server_port,
|
||||
const std::string& pem_certificate,
|
||||
int preverify_ok,
|
||||
int depth)> m_event_loop_dispatcher;
|
||||
bool m_ssl_certificate_callback_done = false;
|
||||
bool m_ssl_certificate_accepted = false;
|
||||
std::shared_ptr<std::mutex> m_mutex;
|
||||
std::shared_ptr<std::condition_variable> m_cond_var;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
void UserClass<T>::session_for_on_disk_path(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
|
||||
auto user = *get_internal<T, UserClass<T>>(this_object);
|
||||
|
@ -332,7 +426,7 @@ void SessionClass<T>::add_progress_notification(ContextType ctx, FunctionType, O
|
|||
validate_argument_count(argc, 3);
|
||||
|
||||
if (auto session = get_internal<T, SessionClass<T>>(this_object)->lock()) {
|
||||
|
||||
|
||||
std::string direction = Value::validated_to_string(ctx, arguments[0], "direction");
|
||||
std::string mode = Value::validated_to_string(ctx, arguments[1], "mode");
|
||||
SyncSession::NotifierType notifierType;
|
||||
|
@ -362,20 +456,20 @@ void SessionClass<T>::add_progress_notification(ContextType ctx, FunctionType, O
|
|||
Protected<FunctionType> protected_callback(ctx, callback_function);
|
||||
Protected<ObjectType> protected_this(ctx, this_object);
|
||||
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
|
||||
std::function<ProgressHandler> progressFunc;
|
||||
std::function<ProgressHandler> progressFunc;
|
||||
|
||||
EventLoopDispatcher<ProgressHandler> progress_handler([=](uint64_t transferred_bytes, uint64_t transferrable_bytes) {
|
||||
HANDLESCOPE
|
||||
ValueType callback_arguments[2];
|
||||
callback_arguments[0] = Value::from_number(protected_ctx, transferred_bytes);
|
||||
callback_arguments[1] = Value::from_number(protected_ctx, transferrable_bytes);
|
||||
|
||||
|
||||
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 2, callback_arguments);
|
||||
});
|
||||
|
||||
progressFunc = std::move(progress_handler);
|
||||
|
||||
|
||||
|
||||
auto registrationToken = session->register_progress_notifier(std::move(progressFunc), notifierType, false);
|
||||
|
||||
auto syncSession = create_object<T, SessionClass<T>>(ctx, new WeakSession(session));
|
||||
|
@ -511,7 +605,7 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
|
|||
static std::regex tilde("/~/");
|
||||
raw_realm_url = std::regex_replace(raw_realm_url, tilde, "/__auth/");
|
||||
}
|
||||
|
||||
|
||||
bool client_validate_ssl = true;
|
||||
ValueType validate_ssl_temp = Object::get_property(ctx, sync_config_object, "validate_ssl");
|
||||
if (!Value::is_undefined(ctx, validate_ssl_temp)) {
|
||||
|
@ -527,12 +621,23 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
|
|||
ssl_trust_certificate_path = util::none;
|
||||
}
|
||||
|
||||
std::function<sync::Session::SSLVerifyCallback> ssl_verify_callback;
|
||||
ValueType ssl_verify_func = Object::get_property(ctx, sync_config_object, "open_ssl_verify_callback");
|
||||
if (!Value::is_undefined(ctx, ssl_verify_func)) {
|
||||
SSLVerifyCallbackSyncThreadFunctor<T> ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)};
|
||||
ssl_verify_callback = std::move(ssl_verify_functor);
|
||||
}
|
||||
|
||||
// FIXME - use make_shared
|
||||
config.sync_config = std::shared_ptr<SyncConfig>(new SyncConfig{shared_user, raw_realm_url,
|
||||
SyncSessionStopPolicy::AfterChangesUploaded,
|
||||
std::move(bind), std::move(error_handler),
|
||||
nullptr, util::none,
|
||||
client_validate_ssl, ssl_trust_certificate_path});
|
||||
client_validate_ssl, ssl_trust_certificate_path,
|
||||
std::move(ssl_verify_callback)});
|
||||
|
||||
|
||||
|
||||
config.schema_mode = SchemaMode::Additive;
|
||||
config.path = realm::SyncManager::shared().path_for_realm(*shared_user, raw_realm_url);
|
||||
|
||||
|
|
Loading…
Reference in New Issue