diff --git a/CHANGELOG.md b/CHANGELOG.md index b04281cc..ff9119c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/realm.js b/docs/realm.js index 63c14ad5..e379d1c7 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -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 + * + * { + * serverAddress: String, + * serverPort: Number, + * pemCertificate: String, + * acceptedByOpenSSL: Boolean, + * depth: Number + * } + * + * 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. */ /** diff --git a/lib/index.d.ts b/lib/index.d.ts index 941fe647..76106fe4 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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; - + /** * @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; - + /** * @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; - + logout(): void; openManagementRealm(): Realm; retrieveAccount(provider: string, username: string): Promise; @@ -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 diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 0ff96000..b364b81b 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "event_loop_dispatcher.hpp" #include "platform.hpp" @@ -240,6 +242,98 @@ private: const Protected 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 +class SSLVerifyCallbackSyncThreadFunctor { +public: + SSLVerifyCallbackSyncThreadFunctor(typename T::Context ctx, typename T::Function ssl_verify_func) + : m_ctx(Context::get_global_context(ctx)) + , m_func(ctx, ssl_verify_func) + , m_event_loop_dispatcher {SSLVerifyCallbackSyncThreadFunctor::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 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 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* 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& ctx = this_object->m_ctx; + + typename T::Object ssl_certificate_object = Object::create_empty(ctx); + Object::set_property(ctx, ssl_certificate_object, "serverAddress", Value::from_string(ctx, server_address)); + Object::set_property(ctx, ssl_certificate_object, "serverPort", Value::from_number(ctx, double(server_port))); + Object::set_property(ctx, ssl_certificate_object, "pemCertificate", Value::from_string(ctx, pem_certificate)); + Object::set_property(ctx, ssl_certificate_object, "acceptedByOpenSSL", Value::from_boolean(ctx, preverify_ok != 0)); + Object::set_property(ctx, ssl_certificate_object, "depth", Value::from_number(ctx, double(depth))); + + const int argc = 1; + typename T::Value arguments[argc] = { ssl_certificate_object }; + typename T::Value ret_val = Function::callback(ctx, this_object->m_func, typename T::Object(), 1, arguments); + bool ret_val_bool = Value::to_boolean(ctx, ret_val); + + { + std::lock_guard 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 m_ctx; + const Protected m_func; + EventLoopDispatcher* 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 m_mutex; + std::shared_ptr m_cond_var; +}; + template void UserClass::session_for_on_disk_path(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { auto user = *get_internal>(this_object); @@ -332,7 +426,7 @@ void SessionClass::add_progress_notification(ContextType ctx, FunctionType, O validate_argument_count(argc, 3); if (auto session = get_internal>(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::add_progress_notification(ContextType ctx, FunctionType, O Protected protected_callback(ctx, callback_function); Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - std::function progressFunc; + std::function progressFunc; EventLoopDispatcher 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::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>(ctx, new WeakSession(session)); @@ -511,7 +605,7 @@ void SyncClass::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::populate_sync_config(ContextType ctx, ObjectType realm_constr ssl_trust_certificate_path = util::none; } + std::function 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 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(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);