diff --git a/atom/app/atom_main_delegate.cc b/atom/app/atom_main_delegate.cc index 2d31b1c91f2e..e4015e57be4b 100644 --- a/atom/app/atom_main_delegate.cc +++ b/atom/app/atom_main_delegate.cc @@ -11,7 +11,9 @@ #include "atom/browser/atom_browser_client.h" #include "atom/browser/relauncher.h" #include "atom/common/google_api_key.h" +#include "atom/common/options_switches.h" #include "atom/renderer/atom_renderer_client.h" +#include "atom/renderer/atom_sandboxed_renderer_client.h" #include "atom/utility/atom_content_utility_client.h" #include "base/command_line.h" #include "base/debug/stack_trace.h" @@ -29,7 +31,7 @@ namespace { const char* kRelauncherProcess = "relauncher"; bool IsBrowserProcess(base::CommandLine* cmd) { - std::string process_type = cmd->GetSwitchValueASCII(switches::kProcessType); + std::string process_type = cmd->GetSwitchValueASCII(::switches::kProcessType); return process_type.empty(); } @@ -72,7 +74,7 @@ bool AtomMainDelegate::BasicStartupComplete(int* exit_code) { // Only enable logging when --enable-logging is specified. std::unique_ptr env(base::Environment::Create()); - if (!command_line->HasSwitch(switches::kEnableLogging) && + if (!command_line->HasSwitch(::switches::kEnableLogging) && !env->HasVar("ELECTRON_ENABLE_LOGGING")) { settings.logging_dest = logging::LOG_NONE; logging::SetMinLogLevel(logging::LOG_NUM_SEVERITIES); @@ -115,17 +117,23 @@ void AtomMainDelegate::PreSandboxStartup() { auto command_line = base::CommandLine::ForCurrentProcess(); std::string process_type = command_line->GetSwitchValueASCII( - switches::kProcessType); + ::switches::kProcessType); // Only append arguments for browser process. if (!IsBrowserProcess(command_line)) return; - // Disable renderer sandbox for most of node's functions. - command_line->AppendSwitch(switches::kNoSandbox); + if (command_line->HasSwitch(switches::kEnableSandbox)) { + // Disable setuid sandbox since it is not longer required on linux(namespace + // sandbox is available on most distros). + command_line->AppendSwitch(::switches::kDisableSetuidSandbox); + } else { + // Disable renderer sandbox for most of node's functions. + command_line->AppendSwitch(::switches::kNoSandbox); + } // Allow file:// URIs to read other file:// URIs by default. - command_line->AppendSwitch(switches::kAllowFileAccessFromFiles); + command_line->AppendSwitch(::switches::kAllowFileAccessFromFiles); #if defined(OS_MACOSX) // Enable AVFoundation. @@ -140,7 +148,13 @@ content::ContentBrowserClient* AtomMainDelegate::CreateContentBrowserClient() { content::ContentRendererClient* AtomMainDelegate::CreateContentRendererClient() { - renderer_client_.reset(new AtomRendererClient); + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kEnableSandbox)) { + renderer_client_.reset(new AtomSandboxedRendererClient); + } else { + renderer_client_.reset(new AtomRendererClient); + } + return renderer_client_.get(); } diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index a5a06b2670d3..8b84b74f59d4 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -258,17 +258,25 @@ void OnCapturePageDone(base::Callback callback, } // namespace WebContents::WebContents(v8::Isolate* isolate, - content::WebContents* web_contents) + content::WebContents* web_contents, + Type type) : content::WebContentsObserver(web_contents), embedder_(nullptr), - type_(REMOTE), + type_(type), request_id_(0), background_throttling_(true), enable_devtools_(true) { - web_contents->SetUserAgentOverride(GetBrowserContext()->GetUserAgent()); - Init(isolate); - AttachAsUserData(web_contents); + if (type == REMOTE) { + web_contents->SetUserAgentOverride(GetBrowserContext()->GetUserAgent()); + Init(isolate); + AttachAsUserData(web_contents); + } else { + const mate::Dictionary options = mate::Dictionary::CreateEmpty(isolate); + auto session = Session::CreateFrom(isolate, GetBrowserContext()); + session_.Reset(isolate, session.ToV8()); + InitWithSessionAndOptions(isolate, web_contents, session, options); + } } WebContents::WebContents(v8::Isolate* isolate, @@ -336,6 +344,13 @@ WebContents::WebContents(v8::Isolate* isolate, web_contents = content::WebContents::Create(params); } + InitWithSessionAndOptions(isolate, web_contents, session, options); +} + +void WebContents::InitWithSessionAndOptions(v8::Isolate* isolate, + content::WebContents *web_contents, + mate::Handle session, + const mate::Dictionary& options) { Observe(web_contents); InitWithWebContents(web_contents, session->browser_context()); @@ -408,6 +423,31 @@ void WebContents::OnCreateWindow(const GURL& target_url, Emit("new-window", target_url, frame_name, disposition); } +void WebContents::WebContentsCreated(content::WebContents* source_contents, + int opener_render_frame_id, + const std::string& frame_name, + const GURL& target_url, + content::WebContents* new_contents) { + v8::Locker locker(isolate()); + v8::HandleScope handle_scope(isolate()); + auto api_web_contents = CreateFrom(isolate(), new_contents, BROWSER_WINDOW); + Emit("-web-contents-created", api_web_contents, target_url, frame_name); +} + +void WebContents::AddNewContents(content::WebContents* source, + content::WebContents* new_contents, + WindowOpenDisposition disposition, + const gfx::Rect& initial_rect, + bool user_gesture, + bool* was_blocked) { + v8::Locker locker(isolate()); + v8::HandleScope handle_scope(isolate()); + auto api_web_contents = CreateFrom(isolate(), new_contents); + Emit("-add-new-contents", api_web_contents, disposition, user_gesture, + initial_rect.x(), initial_rect.y(), initial_rect.width(), + initial_rect.height()); +} + content::WebContents* WebContents::OpenURLFromTab( content::WebContents* source, const content::OpenURLParams& params) { @@ -801,7 +841,14 @@ void WebContents::NavigationEntryCommitted( details.is_in_page, details.did_replace_entry); } -int WebContents::GetID() const { +int64_t WebContents::GetID() const { + int64_t process_id = web_contents()->GetRenderProcessHost()->GetID(); + int64_t routing_id = web_contents()->GetRoutingID(); + int64_t rv = (process_id << 32) + routing_id; + return rv; +} + +int WebContents::GetProcessID() const { return web_contents()->GetRenderProcessHost()->GetID(); } @@ -1496,6 +1543,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) .MakeDestroyable() .SetMethod("getId", &WebContents::GetID) + .SetMethod("getProcessId", &WebContents::GetProcessID) .SetMethod("equal", &WebContents::Equal) .SetMethod("_loadURL", &WebContents::LoadURL) .SetMethod("downloadURL", &WebContents::DownloadURL) @@ -1605,7 +1653,15 @@ mate::Handle WebContents::CreateFrom( return mate::CreateHandle(isolate, static_cast(existing)); // Otherwise create a new WebContents wrapper object. - return mate::CreateHandle(isolate, new WebContents(isolate, web_contents)); + return mate::CreateHandle(isolate, new WebContents(isolate, web_contents, + REMOTE)); +} + +mate::Handle WebContents::CreateFrom( + v8::Isolate* isolate, content::WebContents* web_contents, Type type) { + // Otherwise create a new WebContents wrapper object. + return mate::CreateHandle(isolate, new WebContents(isolate, web_contents, + type)); } // static diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 908a2f0d179f..d0dceb1a98f0 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -58,6 +58,8 @@ class WebContents : public mate::TrackableObject, // Create from an existing WebContents. static mate::Handle CreateFrom( v8::Isolate* isolate, content::WebContents* web_contents); + static mate::Handle CreateFrom( + v8::Isolate* isolate, content::WebContents* web_contents, Type type); // Create a new WebContents. static mate::Handle Create( @@ -66,7 +68,8 @@ class WebContents : public mate::TrackableObject, static void BuildPrototype(v8::Isolate* isolate, v8::Local prototype); - int GetID() const; + int64_t GetID() const; + int GetProcessID() const; Type GetType() const; bool Equal(const WebContents* web_contents) const; void LoadURL(const GURL& url, const mate::Dictionary& options); @@ -191,16 +194,34 @@ class WebContents : public mate::TrackableObject, v8::Local Debugger(v8::Isolate* isolate); protected: - WebContents(v8::Isolate* isolate, content::WebContents* web_contents); + WebContents(v8::Isolate* isolate, + content::WebContents* web_contents, + Type type); WebContents(v8::Isolate* isolate, const mate::Dictionary& options); ~WebContents(); + void InitWithSessionAndOptions(v8::Isolate* isolate, + content::WebContents *web_contents, + mate::Handle session, + const mate::Dictionary& options); + // content::WebContentsDelegate: bool AddMessageToConsole(content::WebContents* source, int32_t level, const base::string16& message, int32_t line_no, const base::string16& source_id) override; + void WebContentsCreated(content::WebContents* source_contents, + int opener_render_frame_id, + const std::string& frame_name, + const GURL& target_url, + content::WebContents* new_contents) override; + void AddNewContents(content::WebContents* source, + content::WebContents* new_contents, + WindowOpenDisposition disposition, + const gfx::Rect& initial_rect, + bool user_gesture, + bool* was_blocked) override; content::WebContents* OpenURLFromTab( content::WebContents* source, const content::OpenURLParams& params) override; diff --git a/atom/browser/api/atom_api_window.cc b/atom/browser/api/atom_api_window.cc index 92e5e5b56280..7c83e558fffa 100644 --- a/atom/browser/api/atom_api_window.cc +++ b/atom/browser/api/atom_api_window.cc @@ -72,20 +72,33 @@ v8::Local ToBuffer(v8::Isolate* isolate, void* val, int size) { Window::Window(v8::Isolate* isolate, v8::Local wrapper, const mate::Dictionary& options) { - // Use options.webPreferences to create WebContents. - mate::Dictionary web_preferences = mate::Dictionary::CreateEmpty(isolate); - options.Get(options::kWebPreferences, &web_preferences); + mate::Handle web_contents; + // If no WebContents was passed to the constructor, create it from options. + if (!options.Get("webContents", &web_contents)) { + // Use options.webPreferences to create WebContents. + mate::Dictionary web_preferences = mate::Dictionary::CreateEmpty(isolate); + options.Get(options::kWebPreferences, &web_preferences); - // Copy the backgroundColor to webContents. - v8::Local value; - if (options.Get(options::kBackgroundColor, &value)) - web_preferences.Set(options::kBackgroundColor, value); + // Copy the backgroundColor to webContents. + v8::Local value; + if (options.Get(options::kBackgroundColor, &value)) + web_preferences.Set(options::kBackgroundColor, value); - v8::Local transparent; - if (options.Get("transparent", &transparent)) - web_preferences.Set("transparent", transparent); - // Creates the WebContents used by BrowserWindow. - auto web_contents = WebContents::Create(isolate, web_preferences); + v8::Local transparent; + if (options.Get("transparent", &transparent)) + web_preferences.Set("transparent", transparent); + + // Creates the WebContents used by BrowserWindow. + web_contents = WebContents::Create(isolate, web_preferences); + } + + Init(isolate, wrapper, options, web_contents); +} + +void Window::Init(v8::Isolate* isolate, + v8::Local wrapper, + const mate::Dictionary& options, + mate::Handle web_contents) { web_contents_.Reset(isolate, web_contents.ToV8()); api_web_contents_ = web_contents.get(); diff --git a/atom/browser/api/atom_api_window.h b/atom/browser/api/atom_api_window.h index d2c41c288165..37508b024fd6 100644 --- a/atom/browser/api/atom_api_window.h +++ b/atom/browser/api/atom_api_window.h @@ -89,6 +89,10 @@ class Window : public mate::TrackableObject, #endif private: + void Init(v8::Isolate* isolate, + v8::Local wrapper, + const mate::Dictionary& options, + mate::Handle web_contents); // APIs for NativeWindow. void Close(); void Focus(); diff --git a/atom/browser/atom_browser_client.cc b/atom/browser/atom_browser_client.cc index edaf20dbd13e..a01ea60f95ae 100644 --- a/atom/browser/atom_browser_client.cc +++ b/atom/browser/atom_browser_client.cc @@ -99,6 +99,44 @@ content::WebContents* AtomBrowserClient::GetWebContentsFromProcessID( return WebContentsPreferences::GetWebContentsFromProcessID(process_id); } +bool AtomBrowserClient::ShouldCreateNewSiteInstance( + content::BrowserContext* browser_context, + content::SiteInstance* current_instance, + const GURL& url) { + + if (url.SchemeIs(url::kJavaScriptScheme)) + // "javacript:" scheme should always use same SiteInstance + return false; + + if (!IsRendererSandboxed(current_instance->GetProcess()->GetID())) + // non-sandboxed renderers should always create a new SiteInstance + return true; + + // Create new a SiteInstance if navigating to a different site. + auto src_url = current_instance->GetSiteURL(); + return + !content::SiteInstance::IsSameWebSite(browser_context, src_url, url) && + // `IsSameWebSite` doesn't seem to work for some URIs such as `file:`, + // handle these scenarios by comparing only the site as defined by + // `GetSiteForURL`. + content::SiteInstance::GetSiteForURL(browser_context, url) != src_url; +} + +void AtomBrowserClient::AddSandboxedRendererId(int process_id) { + base::AutoLock auto_lock(sandboxed_renderers_lock_); + sandboxed_renderers_.insert(process_id); +} + +void AtomBrowserClient::RemoveSandboxedRendererId(int process_id) { + base::AutoLock auto_lock(sandboxed_renderers_lock_); + sandboxed_renderers_.erase(process_id); +} + +bool AtomBrowserClient::IsRendererSandboxed(int process_id) { + base::AutoLock auto_lock(sandboxed_renderers_lock_); + return sandboxed_renderers_.count(process_id); +} + void AtomBrowserClient::RenderProcessWillLaunch( content::RenderProcessHost* host) { int process_id = host->GetID(); @@ -106,6 +144,13 @@ void AtomBrowserClient::RenderProcessWillLaunch( host->AddFilter(new TtsMessageFilter(process_id, host->GetBrowserContext())); host->AddFilter( new WidevineCdmMessageFilter(process_id, host->GetBrowserContext())); + + content::WebContents* web_contents = GetWebContentsFromProcessID(process_id); + if (WebContentsPreferences::IsSandboxed(web_contents)) { + AddSandboxedRendererId(host->GetID()); + // ensure the sandboxed renderer id is removed later + host->AddObserver(this); + } } content::SpeechRecognitionManagerDelegate* @@ -155,8 +200,7 @@ void AtomBrowserClient::OverrideSiteInstanceForNavigation( return; } - // Restart renderer process for all navigations except "javacript:" scheme. - if (url.SchemeIs(url::kJavaScriptScheme)) + if (!ShouldCreateNewSiteInstance(browser_context, current_instance, url)) return; scoped_refptr site_instance = @@ -188,6 +232,7 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches( // Copy following switches to child process. static const char* const kCommonSwitchNames[] = { switches::kStandardSchemes, + switches::kEnableSandbox }; command_line->CopySwitchesFrom( *base::CommandLine::ForCurrentProcess(), @@ -281,6 +326,11 @@ bool AtomBrowserClient::CanCreateWindow( bool* no_javascript_access) { DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (IsRendererSandboxed(render_process_id)) { + *no_javascript_access = false; + return true; + } + if (delegate_) { content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE, base::Bind(&api::App::OnCreateWindow, @@ -338,6 +388,7 @@ void AtomBrowserClient::RenderProcessHostDestroyed( break; } } + RemoveSandboxedRendererId(process_id); } } // namespace atom diff --git a/atom/browser/atom_browser_client.h b/atom/browser/atom_browser_client.h index 5a43dbad30b7..b6e928ce8e69 100644 --- a/atom/browser/atom_browser_client.h +++ b/atom/browser/atom_browser_client.h @@ -6,6 +6,7 @@ #define ATOM_BROWSER_ATOM_BROWSER_CLIENT_H_ #include +#include #include #include @@ -108,8 +109,19 @@ class AtomBrowserClient : public brightray::BrowserClient, void RenderProcessHostDestroyed(content::RenderProcessHost* host) override; private: + bool ShouldCreateNewSiteInstance(content::BrowserContext* browser_context, + content::SiteInstance* current_instance, + const GURL& dest_url); + // Add/remove a process id to `sandboxed_renderers_`. + void AddSandboxedRendererId(int process_id); + void RemoveSandboxedRendererId(int process_id); + bool IsRendererSandboxed(int process_id); + // pending_render_process => current_render_process. std::map pending_processes_; + // Set that contains the process ids of all sandboxed renderers + std::set sandboxed_renderers_; + base::Lock sandboxed_renderers_lock_; std::unique_ptr resource_dispatcher_host_delegate_; diff --git a/atom/browser/native_window.cc b/atom/browser/native_window.cc index 728053323754..1cf5a23b6efe 100644 --- a/atom/browser/native_window.cc +++ b/atom/browser/native_window.cc @@ -391,6 +391,10 @@ void NativeWindow::RequestToClosePage() { if (window_unresposive_closure_.IsCancelled()) ScheduleUnresponsiveEvent(5000); + if (!web_contents()) + // Already closed by renderer + return; + if (web_contents()->NeedToFireBeforeUnload()) web_contents()->DispatchBeforeUnload(); else diff --git a/atom/browser/web_contents_preferences.cc b/atom/browser/web_contents_preferences.cc index 5628c19e9e9a..a9e7fb448435 100644 --- a/atom/browser/web_contents_preferences.cc +++ b/atom/browser/web_contents_preferences.cc @@ -97,6 +97,12 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches( command_line->AppendSwitchASCII(switches::kNodeIntegration, node_integration ? "true" : "false"); + // If the `sandbox` option was passed to the BrowserWindow's webPreferences, + // pass `--enable-sandbox` to the renderer so it won't have any node.js + // integration. + if (IsSandboxed(web_contents)) + command_line->AppendSwitch(switches::kEnableSandbox); + // The preload script. base::FilePath::StringType preload; if (web_preferences.GetString(options::kPreloadScript, &preload)) { @@ -194,6 +200,21 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches( command_line->AppendSwitch(cc::switches::kEnableBeginFrameScheduling); } +bool WebContentsPreferences::IsSandboxed(content::WebContents* web_contents) { + WebContentsPreferences* self; + if (!web_contents) + return false; + + self = FromWebContents(web_contents); + if (!self) + return false; + + base::DictionaryValue& web_preferences = self->web_preferences_; + bool sandboxed = false; + web_preferences.GetBoolean("sandbox", &sandboxed); + return sandboxed; +} + // static void WebContentsPreferences::OverrideWebkitPrefs( content::WebContents* web_contents, content::WebPreferences* prefs) { diff --git a/atom/browser/web_contents_preferences.h b/atom/browser/web_contents_preferences.h index daf1f6e84de5..3a04ea9eea0b 100644 --- a/atom/browser/web_contents_preferences.h +++ b/atom/browser/web_contents_preferences.h @@ -36,6 +36,8 @@ class WebContentsPreferences static void AppendExtraCommandLineSwitches( content::WebContents* web_contents, base::CommandLine* command_line); + static bool IsSandboxed(content::WebContents* web_contents); + // Modify the WebPreferences according to |web_contents|'s preferences. static void OverrideWebkitPrefs( content::WebContents* web_contents, content::WebPreferences* prefs); diff --git a/atom/common/api/atom_api_v8_util.cc b/atom/common/api/atom_api_v8_util.cc index 7b7655c6cd2e..ddacbb080899 100644 --- a/atom/common/api/atom_api_v8_util.cc +++ b/atom/common/api/atom_api_v8_util.cc @@ -104,7 +104,7 @@ void Initialize(v8::Local exports, v8::Local unused, dict.SetMethod("setRemoteObjectFreer", &atom::RemoteObjectFreer::BindTo); dict.SetMethod("createIDWeakMap", &atom::api::KeyWeakMap::Create); dict.SetMethod("createDoubleIDWeakMap", - &atom::api::KeyWeakMap>::Create); + &atom::api::KeyWeakMap>::Create); } } // namespace diff --git a/atom/common/options_switches.cc b/atom/common/options_switches.cc index 6b514599f085..599efbee4296 100644 --- a/atom/common/options_switches.cc +++ b/atom/common/options_switches.cc @@ -117,6 +117,9 @@ const char kDisableBlinkFeatures[] = "disableBlinkFeatures"; namespace switches { +// Enable chromium sandbox. +const char kEnableSandbox[] = "enable-sandbox"; + // Enable plugins. const char kEnablePlugins[] = "enable-plugins"; diff --git a/atom/common/options_switches.h b/atom/common/options_switches.h index 54c638772887..259f4e17b307 100644 --- a/atom/common/options_switches.h +++ b/atom/common/options_switches.h @@ -67,6 +67,7 @@ extern const char kDisableBlinkFeatures[]; namespace switches { +extern const char kEnableSandbox[]; extern const char kEnablePlugins[]; extern const char kPpapiFlashPath[]; extern const char kPpapiFlashVersion[]; diff --git a/atom/renderer/api/atom_api_renderer_ipc.cc b/atom/renderer/api/atom_api_renderer_ipc.cc index 49f917292bb2..281480c8e5b2 100644 --- a/atom/renderer/api/atom_api_renderer_ipc.cc +++ b/atom/renderer/api/atom_api_renderer_ipc.cc @@ -2,6 +2,7 @@ // Use of this source code is governed by the MIT license that can be // found in the LICENSE file. +#include "atom/renderer/api/atom_api_renderer_ipc.h" #include "atom/common/api/api_messages.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "atom/common/native_mate_converters/value_converter.h" @@ -15,7 +16,9 @@ using content::RenderView; using blink::WebLocalFrame; using blink::WebView; -namespace { +namespace atom { + +namespace api { RenderView* GetCurrentRenderView() { WebLocalFrame* frame = WebLocalFrame::frameForCurrentContext(); @@ -69,6 +72,8 @@ void Initialize(v8::Local exports, v8::Local unused, dict.SetMethod("sendSync", &SendSync); } -} // namespace +} // namespace api -NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_renderer_ipc, Initialize) +} // namespace atom + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_renderer_ipc, atom::api::Initialize) diff --git a/atom/renderer/api/atom_api_renderer_ipc.h b/atom/renderer/api/atom_api_renderer_ipc.h new file mode 100644 index 000000000000..0f2105ba557a --- /dev/null +++ b/atom/renderer/api/atom_api_renderer_ipc.h @@ -0,0 +1,30 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_RENDERER_API_ATOM_API_RENDERER_IPC_H_ +#define ATOM_RENDERER_API_ATOM_API_RENDERER_IPC_H_ + +#include "base/values.h" +#include "native_mate/arguments.h" + +namespace atom { + +namespace api { + +void Send(mate::Arguments* args, + const base::string16& channel, + const base::ListValue& arguments); + +base::string16 SendSync(mate::Arguments* args, + const base::string16& channel, + const base::ListValue& arguments); + +void Initialize(v8::Local exports, v8::Local unused, + v8::Local context, void* priv); + +} // namespace api + +} // namespace atom + +#endif // ATOM_RENDERER_API_ATOM_API_RENDERER_IPC_H_ diff --git a/atom/renderer/atom_render_view_observer.cc b/atom/renderer/atom_render_view_observer.cc index 0db68e14dc44..4600cf9abdd9 100644 --- a/atom/renderer/atom_render_view_observer.cc +++ b/atom/renderer/atom_render_view_observer.cc @@ -59,9 +59,33 @@ std::vector> ListValueToVector( return result; } -void EmitIPCEvent(blink::WebFrame* frame, - const base::string16& channel, - const base::ListValue& args) { +base::StringPiece NetResourceProvider(int key) { + if (key == IDR_DIR_HEADER_HTML) { + base::StringPiece html_data = + ui::ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_DIR_HEADER_HTML); + return html_data; + } + return base::StringPiece(); +} + +} // namespace + +AtomRenderViewObserver::AtomRenderViewObserver( + content::RenderView* render_view, + AtomRendererClient* renderer_client) + : content::RenderViewObserver(render_view), + document_created_(false) { + // Initialise resource for directory listing. + net::NetModule::SetResourceProvider(NetResourceProvider); +} + +AtomRenderViewObserver::~AtomRenderViewObserver() { +} + +void AtomRenderViewObserver::EmitIPCEvent(blink::WebFrame* frame, + const base::string16& channel, + const base::ListValue& args) { if (!frame || frame->isWebRemoteFrame()) return; @@ -87,30 +111,6 @@ void EmitIPCEvent(blink::WebFrame* frame, } } -base::StringPiece NetResourceProvider(int key) { - if (key == IDR_DIR_HEADER_HTML) { - base::StringPiece html_data = - ui::ResourceBundle::GetSharedInstance().GetRawDataResource( - IDR_DIR_HEADER_HTML); - return html_data; - } - return base::StringPiece(); -} - -} // namespace - -AtomRenderViewObserver::AtomRenderViewObserver( - content::RenderView* render_view, - AtomRendererClient* renderer_client) - : content::RenderViewObserver(render_view), - document_created_(false) { - // Initialise resource for directory listing. - net::NetModule::SetResourceProvider(NetResourceProvider); -} - -AtomRenderViewObserver::~AtomRenderViewObserver() { -} - void AtomRenderViewObserver::DidCreateDocumentElement( blink::WebLocalFrame* frame) { document_created_ = true; diff --git a/atom/renderer/atom_render_view_observer.h b/atom/renderer/atom_render_view_observer.h index 76b05cd19ec9..ce426060b660 100644 --- a/atom/renderer/atom_render_view_observer.h +++ b/atom/renderer/atom_render_view_observer.h @@ -7,6 +7,7 @@ #include "base/strings/string16.h" #include "content/public/renderer/render_view_observer.h" +#include "third_party/WebKit/public/web/WebFrame.h" namespace base { class ListValue; @@ -24,6 +25,10 @@ class AtomRenderViewObserver : public content::RenderViewObserver { protected: virtual ~AtomRenderViewObserver(); + virtual void EmitIPCEvent(blink::WebFrame* frame, + const base::string16& channel, + const base::ListValue& args); + private: // content::RenderViewObserver implementation. void DidCreateDocumentElement(blink::WebLocalFrame* frame) override; diff --git a/atom/renderer/atom_sandboxed_renderer_client.cc b/atom/renderer/atom_sandboxed_renderer_client.cc new file mode 100644 index 000000000000..47d9d0ded6c6 --- /dev/null +++ b/atom/renderer/atom_sandboxed_renderer_client.cc @@ -0,0 +1,201 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/renderer/atom_sandboxed_renderer_client.h" + +#include + +#include "atom_natives.h" // NOLINT: This file is generated with js2c + +#include "atom/common/api/api_messages.h" +#include "atom/common/native_mate_converters/string16_converter.h" +#include "atom/common/native_mate_converters/value_converter.h" +#include "atom/common/options_switches.h" +#include "atom/renderer/api/atom_api_renderer_ipc.h" +#include "atom/renderer/atom_render_view_observer.h" +#include "base/command_line.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "content/public/renderer/render_view.h" +#include "content/public/renderer/render_view_observer.h" +#include "ipc/ipc_message_macros.h" +#include "third_party/WebKit/public/web/WebFrame.h" +#include "third_party/WebKit/public/web/WebKit.h" +#include "third_party/WebKit/public/web/WebLocalFrame.h" +#include "third_party/WebKit/public/web/WebScriptSource.h" +#include "third_party/WebKit/public/web/WebView.h" + +namespace atom { + +namespace { + +const std::string kBindingKey = "binding"; + +class AtomSandboxedRenderFrameObserver : public content::RenderFrameObserver { + public: + AtomSandboxedRenderFrameObserver(content::RenderFrame* frame, + AtomSandboxedRendererClient* renderer_client) + : content::RenderFrameObserver(frame), + render_frame_(frame), + world_id_(-1), + renderer_client_(renderer_client) {} + + // content::RenderFrameObserver: + void DidClearWindowObject() override { + // Make sure every page will get a script context created. + render_frame_->GetWebFrame()->executeScript( + blink::WebScriptSource("void 0")); + } + + void DidCreateScriptContext(v8::Handle context, + int extension_group, + int world_id) override { + if (world_id_ != -1 && world_id_ != world_id) + return; + world_id_ = world_id; + renderer_client_->DidCreateScriptContext(context, render_frame_); + } + + void WillReleaseScriptContext(v8::Local context, + int world_id) override { + if (world_id_ != world_id) + return; + renderer_client_->WillReleaseScriptContext(context, render_frame_); + } + + void OnDestruct() override { + delete this; + } + + private: + content::RenderFrame* render_frame_; + int world_id_; + AtomSandboxedRendererClient* renderer_client_; + + DISALLOW_COPY_AND_ASSIGN(AtomSandboxedRenderFrameObserver); +}; + +class AtomSandboxedRenderViewObserver : public AtomRenderViewObserver { + public: + AtomSandboxedRenderViewObserver(content::RenderView* render_view, + AtomSandboxedRendererClient* renderer_client) + : AtomRenderViewObserver(render_view, nullptr), + renderer_client_(renderer_client) { + } + + protected: + void EmitIPCEvent(blink::WebFrame* frame, + const base::string16& channel, + const base::ListValue& args) override { + if (!frame || frame->isWebRemoteFrame()) + return; + + auto isolate = blink::mainThreadIsolate(); + v8::HandleScope handle_scope(isolate); + auto context = frame->mainWorldScriptContext(); + v8::Context::Scope context_scope(context); + v8::Local argv[] = { + mate::ConvertToV8(isolate, channel), + mate::ConvertToV8(isolate, args) + }; + renderer_client_->InvokeBindingCallback( + context, + "onMessage", + std::vector>(argv, argv + 2)); + } + + private: + AtomSandboxedRendererClient* renderer_client_; + DISALLOW_COPY_AND_ASSIGN(AtomSandboxedRenderViewObserver); +}; + +} // namespace + + +AtomSandboxedRendererClient::AtomSandboxedRendererClient() { +} + +AtomSandboxedRendererClient::~AtomSandboxedRendererClient() { +} + +void AtomSandboxedRendererClient::RenderFrameCreated( + content::RenderFrame* render_frame) { + new AtomSandboxedRenderFrameObserver(render_frame, this); +} + +void AtomSandboxedRendererClient::RenderViewCreated( + content::RenderView* render_view) { + new AtomSandboxedRenderViewObserver(render_view, this); +} + +void AtomSandboxedRendererClient::DidCreateScriptContext( + v8::Handle context, content::RenderFrame* render_frame) { + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + std::string preload_script = command_line->GetSwitchValueASCII( + switches::kPreloadScript); + if (preload_script.empty()) + return; + + auto isolate = context->GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context); + // Wrap the bundle into a function that receives the binding object and the + // preload script path as arguments. + std::string preload_bundle_native(node::preload_bundle_native, + node::preload_bundle_native + sizeof(node::preload_bundle_native)); + std::stringstream ss; + ss << "(function(binding, preloadPath) {\n"; + ss << preload_bundle_native << "\n"; + ss << "})"; + std::string preload_wrapper = ss.str(); + // Compile the wrapper and run it to get the function object + auto script = v8::Script::Compile( + mate::ConvertToV8(isolate, preload_wrapper)->ToString()); + auto func = v8::Handle::Cast( + script->Run(context).ToLocalChecked()); + // Create and initialize the binding object + auto binding = v8::Object::New(isolate); + api::Initialize(binding, v8::Null(isolate), context, nullptr); + v8::Local args[] = { + binding, + mate::ConvertToV8(isolate, preload_script) + }; + // Execute the function with proper arguments + ignore_result(func->Call(context, v8::Null(isolate), 2, args)); + // Store the bindingt privately for handling messages from the main process. + auto binding_key = mate::ConvertToV8(isolate, kBindingKey)->ToString(); + auto private_binding_key = v8::Private::ForApi(isolate, binding_key); + context->Global()->SetPrivate(context, private_binding_key, binding); +} + +void AtomSandboxedRendererClient::WillReleaseScriptContext( + v8::Handle context, content::RenderFrame* render_frame) { + auto isolate = context->GetIsolate(); + v8::HandleScope handle_scope(isolate); + v8::Context::Scope context_scope(context); + InvokeBindingCallback(context, "onExit", std::vector>()); +} + +void AtomSandboxedRendererClient::InvokeBindingCallback( + v8::Handle context, + std::string callback_name, + std::vector> args) { + auto isolate = context->GetIsolate(); + auto binding_key = mate::ConvertToV8(isolate, kBindingKey)->ToString(); + auto private_binding_key = v8::Private::ForApi(isolate, binding_key); + auto global_object = context->Global(); + v8::Local value; + if (!global_object->GetPrivate(context, private_binding_key).ToLocal(&value)) + return; + if (value.IsEmpty() || !value->IsObject()) + return; + auto binding = value->ToObject(); + auto callback_key = mate::ConvertToV8(isolate, callback_name)->ToString(); + auto callback_value = binding->Get(callback_key); + DCHECK(callback_value->IsFunction()); // set by sandboxed_renderer/init.js + auto callback = v8::Handle::Cast(callback_value); + ignore_result(callback->Call(context, binding, args.size(), &args[0])); +} + +} // namespace atom diff --git a/atom/renderer/atom_sandboxed_renderer_client.h b/atom/renderer/atom_sandboxed_renderer_client.h new file mode 100644 index 000000000000..66d827891550 --- /dev/null +++ b/atom/renderer/atom_sandboxed_renderer_client.h @@ -0,0 +1,37 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. +#ifndef ATOM_RENDERER_ATOM_SANDBOXED_RENDERER_CLIENT_H_ +#define ATOM_RENDERER_ATOM_SANDBOXED_RENDERER_CLIENT_H_ + +#include +#include + +#include "content/public/renderer/content_renderer_client.h" +#include "content/public/renderer/render_frame.h" + +namespace atom { + +class AtomSandboxedRendererClient : public content::ContentRendererClient { + public: + AtomSandboxedRendererClient(); + virtual ~AtomSandboxedRendererClient(); + + void DidCreateScriptContext( + v8::Handle context, content::RenderFrame* render_frame); + void WillReleaseScriptContext( + v8::Handle context, content::RenderFrame* render_frame); + void InvokeBindingCallback(v8::Handle context, + std::string callback_name, + std::vector> args); + // content::ContentRendererClient: + void RenderFrameCreated(content::RenderFrame*) override; + void RenderViewCreated(content::RenderView*) override; + + private: + DISALLOW_COPY_AND_ASSIGN(AtomSandboxedRendererClient); +}; + +} // namespace atom + +#endif // ATOM_RENDERER_ATOM_SANDBOXED_RENDERER_CLIENT_H_ diff --git a/electron.gyp b/electron.gyp index 5d44f8d1bb96..f15ed8e2c2de 100644 --- a/electron.gyp +++ b/electron.gyp @@ -5,6 +5,7 @@ 'company_name%': 'GitHub, Inc', 'company_abbr%': 'github', 'version%': '1.4.1', + 'js2c_input_dir': '<(SHARED_INTERMEDIATE_DIR)/js2c', }, 'includes': [ 'filenames.gypi', @@ -410,14 +411,64 @@ } ], }, # target app2asar + { + 'target_name': 'atom_js2c_copy', + 'type': 'none', + 'copies': [ + { + 'destination': '<(js2c_input_dir)', + 'files': [ + '<@(js2c_sources)', + ], + }, + ], + }, # target atom_js2c_copy + { + 'target_name': 'atom_browserify', + 'type': 'none', + 'dependencies': [ + # depend on this target to ensure the '<(js2c_input_dir)' is created + 'atom_js2c_copy', + ], + 'actions': [ + { + 'action_name': 'atom_browserify', + 'inputs': [ + '<@(browserify_entries)', + # Any js file under `lib/` can be included in the preload bundle. + # Add all js sources as dependencies so any change to a js file will + # trigger a rebuild of the bundle(and consequently of js2c). + '<@(js_sources)', + ], + 'outputs': [ + '<(js2c_input_dir)/preload_bundle.js', + ], + 'action': [ + 'npm', + 'run', + 'browserify', + '--', + '<@(browserify_entries)', + '-o', + '<@(_outputs)', + ], + } + ], + }, # target atom_browserify { 'target_name': 'atom_js2c', 'type': 'none', + 'dependencies': [ + 'atom_js2c_copy', + 'atom_browserify', + ], 'actions': [ { 'action_name': 'atom_js2c', 'inputs': [ + # List all input files that should trigger a rebuild with js2c '<@(js2c_sources)', + '<(js2c_input_dir)/preload_bundle.js', ], 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/atom_natives.h', @@ -426,7 +477,7 @@ 'python', 'tools/js2c.py', '<@(_outputs)', - '<@(_inputs)', + '<(js2c_input_dir)', ], } ], diff --git a/filenames.gypi b/filenames.gypi index f8d80c0fda4e..d8b3ede3c59a 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -61,6 +61,7 @@ 'lib/renderer/api/desktop-capturer.js', 'lib/renderer/api/exports/electron.js', 'lib/renderer/api/ipc-renderer.js', + 'lib/renderer/api/ipc-renderer-setup.js', 'lib/renderer/api/remote.js', 'lib/renderer/api/screen.js', 'lib/renderer/api/web-frame.js', @@ -69,6 +70,9 @@ 'lib/renderer/extensions/storage.js', 'lib/renderer/extensions/web-navigation.js', ], + 'browserify_entries': [ + 'lib/sandboxed_renderer/init.js', + ], 'js2c_sources': [ 'lib/common/asar.js', 'lib/common/asar_init.js', @@ -424,6 +428,7 @@ 'atom/common/platform_util_linux.cc', 'atom/common/platform_util_mac.mm', 'atom/common/platform_util_win.cc', + 'atom/renderer/api/atom_api_renderer_ipc.h', 'atom/renderer/api/atom_api_renderer_ipc.cc', 'atom/renderer/api/atom_api_spell_check_client.cc', 'atom/renderer/api/atom_api_spell_check_client.h', @@ -435,6 +440,8 @@ 'atom/renderer/atom_renderer_client.h', 'atom/renderer/content_settings_observer.cc', 'atom/renderer/content_settings_observer.h', + 'atom/renderer/atom_sandboxed_renderer_client.cc', + 'atom/renderer/atom_sandboxed_renderer_client.h', 'atom/renderer/guest_view_container.cc', 'atom/renderer/guest_view_container.h', 'atom/renderer/node_array_buffer_bridge.cc', diff --git a/lib/browser/api/browser-window.js b/lib/browser/api/browser-window.js index ecfdda05f8db..920d2404432e 100644 --- a/lib/browser/api/browser-window.js +++ b/lib/browser/api/browser-window.js @@ -3,6 +3,7 @@ const {ipcMain} = require('electron') const {EventEmitter} = require('events') const {BrowserWindow} = process.atomBinding('window') +const v8Util = process.atomBinding('v8_util') Object.setPrototypeOf(BrowserWindow.prototype, EventEmitter.prototype) @@ -26,6 +27,34 @@ BrowserWindow.prototype._init = function () { ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, disposition, options) }) + this.webContents.on('-web-contents-created', (event, webContents, url, + frameName) => { + v8Util.setHiddenValue(webContents, 'url-framename', {url, frameName}) + }) + // Create a new browser window for the native implementation of + // "window.open"(sandbox mode only) + this.webContents.on('-add-new-contents', (event, webContents, disposition, + userGesture, left, top, width, + height) => { + let urlFrameName = v8Util.getHiddenValue(webContents, 'url-framename') + if ((disposition !== 'foreground-tab' && disposition !== 'new-window') || + !urlFrameName) { + return + } + + let {url, frameName} = urlFrameName + v8Util.deleteHiddenValue(webContents, 'url-framename') + const options = { + show: true, + x: left, + y: top, + width: width || 800, + height: height || 600, + webContents: webContents + } + ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, disposition, options) + }) + // window.resizeTo(...) // window.moveTo(...) this.webContents.on('move', (event, size) => { diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 2088f39b37e7..840715a08a17 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -123,7 +123,7 @@ const hookWebContentsEvents = function (webContents) { sendToBackgroundPages('CHROME_WEBNAVIGATION_ONBEFORENAVIGATE', { frameId: 0, parentFrameId: -1, - processId: webContents.getId(), + processId: webContents.getProcessId(), tabId: tabId, timeStamp: Date.now(), url: url @@ -134,7 +134,7 @@ const hookWebContentsEvents = function (webContents) { sendToBackgroundPages('CHROME_WEBNAVIGATION_ONCOMPLETED', { frameId: 0, parentFrameId: -1, - processId: webContents.getId(), + processId: webContents.getProcessId(), tabId: tabId, timeStamp: Date.now(), url: url diff --git a/lib/browser/guest-window-manager.js b/lib/browser/guest-window-manager.js index 776ab91fec96..f20f1bbb0ab9 100644 --- a/lib/browser/guest-window-manager.js +++ b/lib/browser/guest-window-manager.js @@ -57,7 +57,30 @@ const createGuest = function (embedder, url, frameName, options) { } options.webPreferences.openerId = embedder.id guest = new BrowserWindow(options) - guest.loadURL(url) + if (!options.webContents || url !== 'about:blank') { + // We should not call `loadURL` if the window was constructed from an + // existing webContents(window.open in a sandboxed renderer) and if the url + // is not 'about:blank'. + // + // Navigating to the url when creating the window from an existing + // webContents would not be necessary(it will navigate there anyway), but + // apparently there's a bug that allows the child window to be scripted by + // the opener, even when the child window is from another origin. + // + // That's why the second condition(url !== "about:blank") is required: to + // force `OverrideSiteInstanceForNavigation` to be called and consequently + // spawn a new renderer if the new window is targeting a different origin. + // + // If the URL is "about:blank", then it is very likely that the opener just + // wants to synchronously script the popup, for example: + // + // let popup = window.open() + // popup.document.body.write('

hello

') + // + // The above code would not work if a navigation to "about:blank" is done + // here, since the window would be cleared of all changes in the next tick. + guest.loadURL(url) + } // When |embedder| is destroyed we should also destroy attached guest, and if // guest is closed by user then we should prevent |embedder| from double @@ -72,8 +95,19 @@ const createGuest = function (embedder, url, frameName, options) { embedder.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId) embedder.removeListener('render-view-deleted', closedByEmbedder) } - embedder.once('render-view-deleted', closedByEmbedder) - guest.once('closed', closedByUser) + if (!options.webPreferences.sandbox) { + // These events should only be handled when the guest window is opened by a + // non-sandboxed renderer for two reasons: + // + // - `render-view-deleted` is emitted when the popup is closed by the user, + // and that will eventually result in NativeWindow::NotifyWindowClosed + // using a dangling pointer since `destroy()` would have been called by + // `closeByEmbedded` + // - No need to emit `ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_` since + // there's no renderer code listening to it., + embedder.once('render-view-deleted', closedByEmbedder) + guest.once('closed', closedByUser) + } if (frameName) { frameToGuest[frameName] = guest diff --git a/lib/browser/rpc-server.js b/lib/browser/rpc-server.js index 31e5a37732c7..447b12e3b283 100644 --- a/lib/browser/rpc-server.js +++ b/lib/browser/rpc-server.js @@ -5,6 +5,8 @@ const electron = require('electron') const v8Util = process.atomBinding('v8_util') const {ipcMain, isPromise, webContents} = electron +const fs = require('fs') + const objectsRegistry = require('./objects-registry') const hasProp = {}.hasOwnProperty @@ -235,6 +237,13 @@ ipcMain.on('ELECTRON_BROWSER_REQUIRE', function (event, module) { } }) +ipcMain.on('ELECTRON_BROWSER_READ_FILE', function (event, file) { + fs.readFile(file, (err, data) => { + if (err) event.returnValue = {err: err.message} + else event.returnValue = {data: data.toString()} + }) +}) + ipcMain.on('ELECTRON_BROWSER_GET_BUILTIN', function (event, module) { try { event.returnValue = valueToMeta(event.sender, electron[module]) diff --git a/lib/renderer/api/ipc-renderer-setup.js b/lib/renderer/api/ipc-renderer-setup.js new file mode 100644 index 000000000000..c4459df2bf48 --- /dev/null +++ b/lib/renderer/api/ipc-renderer-setup.js @@ -0,0 +1,29 @@ +module.exports = function (ipcRenderer, binding) { + ipcRenderer.send = function (...args) { + return binding.send('ipc-message', args) + } + + ipcRenderer.sendSync = function (...args) { + return JSON.parse(binding.sendSync('ipc-message-sync', args)) + } + + ipcRenderer.sendToHost = function (...args) { + return binding.send('ipc-message-host', args) + } + + ipcRenderer.sendTo = function (webContentsId, channel, ...args) { + if (typeof webContentsId !== 'number') { + throw new TypeError('First argument has to be webContentsId') + } + + ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args) + } + + ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { + if (typeof webContentsId !== 'number') { + throw new TypeError('First argument has to be webContentsId') + } + + ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args) + } +} diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index 66c40d311da2..0a84a0d8ba19 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -5,33 +5,6 @@ const v8Util = process.atomBinding('v8_util') // Created by init.js. const ipcRenderer = v8Util.getHiddenValue(global, 'ipc') - -ipcRenderer.send = function (...args) { - return binding.send('ipc-message', args) -} - -ipcRenderer.sendSync = function (...args) { - return JSON.parse(binding.sendSync('ipc-message-sync', args)) -} - -ipcRenderer.sendToHost = function (...args) { - return binding.send('ipc-message-host', args) -} - -ipcRenderer.sendTo = function (webContentsId, channel, ...args) { - if (typeof webContentsId !== 'number') { - throw new TypeError('First argument has to be webContentsId') - } - - ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args) -} - -ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { - if (typeof webContentsId !== 'number') { - throw new TypeError('First argument has to be webContentsId') - } - - ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args) -} +require('./ipc-renderer-setup')(ipcRenderer, binding) module.exports = ipcRenderer diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js new file mode 100644 index 000000000000..e34b7bc70759 --- /dev/null +++ b/lib/sandboxed_renderer/init.js @@ -0,0 +1,56 @@ +/* eslint no-eval: "off" */ +/* global binding, preloadPath, process, Buffer */ +const events = require('events') + +const ipcRenderer = new events.EventEmitter() +const proc = new events.EventEmitter() +// eval in window scope: +// http://www.ecma-international.org/ecma-262/5.1/#sec-10.4.2 +const geval = eval + +require('../renderer/api/ipc-renderer-setup')(ipcRenderer, binding) + +binding.onMessage = function (channel, args) { + ipcRenderer.emit.apply(ipcRenderer, [channel].concat(args)) +} + +binding.onExit = function () { + proc.emit('exit') +} + +const preloadModules = new Map([ + ['electron', { + ipcRenderer: ipcRenderer + }] +]) + +function preloadRequire (module) { + if (preloadModules.has(module)) { + return preloadModules.get(module) + } + throw new Error('module not found') +} + +// Fetch the source for the preload +let preloadSrc = ipcRenderer.sendSync('ELECTRON_BROWSER_READ_FILE', preloadPath) +if (preloadSrc.err) { + throw new Error(preloadSrc.err) +} + +// Wrap the source into a function receives a `require` function as argument. +// Browserify bundles can make use of this, as explained in: +// https://github.com/substack/node-browserify#multiple-bundles +// +// For example, the user can create a browserify bundle with: +// +// $ browserify -x electron preload.js > renderer.js +// +// and any `require('electron')` calls in `preload.js` will work as expected +// since browserify won't try to include `electron` in the bundle and will fall +// back to the `preloadRequire` function above. +let preloadWrapperSrc = `(function(require, process, Buffer, global) { +${preloadSrc.data} +})` + +let preloadFn = geval(preloadWrapperSrc) +preloadFn(preloadRequire, proc, Buffer, global) diff --git a/package.json b/package.json index 25bde883feca..2fd5bec30542 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.4.1", "devDependencies": { "asar": "^0.11.0", + "browserify": "^13.1.0", "electabul": "~0.0.4", "electron-docs-linter": "^1.6.2", "request": "*", @@ -25,6 +26,7 @@ "private": true, "scripts": { "bootstrap": "python ./script/bootstrap.py", + "browserify": "browserify", "build": "python ./script/build.py -c D", "clean": "python ./script/clean.py", "coverage": "npm run instrument-code-coverage && npm test -- --use-instrumented-asar", diff --git a/script/bootstrap.py b/script/bootstrap.py index 37f4ec21de94..f23884f8afde 100755 --- a/script/bootstrap.py +++ b/script/bootstrap.py @@ -14,10 +14,7 @@ SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) VENDOR_DIR = os.path.join(SOURCE_ROOT, 'vendor') PYTHON_26_URL = 'https://chromium.googlesource.com/chromium/deps/python_26' -if os.environ.has_key('CI'): - NPM = os.path.join(SOURCE_ROOT, 'node_modules', '.bin', 'npm') -else: - NPM = 'npm' +NPM = 'npm' if sys.platform in ['win32', 'cygwin']: NPM += '.cmd' diff --git a/script/cibuild b/script/cibuild index ca3cb8db34e2..92dec9db2dee 100755 --- a/script/cibuild +++ b/script/cibuild @@ -65,6 +65,11 @@ def main(): execute([npm, 'install', 'npm@2.12.1']) log_versions() + # Add "./node_modules/.bin" to the beginning of $PATH, which will ensure + # future "npm" invocations use the right version. + node_bin_dir = os.path.join(SOURCE_ROOT, 'node_modules', '.bin') + os.environ['PATH'] = os.path.pathsep.join([node_bin_dir, + os.environ.get('PATH', '')]) is_release = os.environ.has_key('ELECTRON_RELEASE') args = ['--target_arch=' + target_arch] diff --git a/spec/api-browser-window-spec.js b/spec/api-browser-window-spec.js index f7d30098b5f4..23c426aa9eb1 100644 --- a/spec/api-browser-window-spec.js +++ b/spec/api-browser-window-spec.js @@ -16,6 +16,7 @@ const ipcRenderer = require('electron').ipcRenderer const BrowserWindow = remote.require('electron').BrowserWindow const isCI = remote.getGlobal('isCi') +const {protocol} = remote describe('browser-window module', function () { var fixtures = path.resolve(__dirname, 'fixtures') @@ -572,6 +573,198 @@ describe('browser-window module', function () { w.loadURL('file://' + path.join(fixtures, 'api', 'blank.html')) }) }) + + describe('"sandbox" option', function () { + function waitForEvents (emitter, events, callback) { + let count = events.length + for (let event of events) { + emitter.once(event, () => { + if (!--count) callback() + }) + } + } + + const preload = path.join(fixtures, 'module', 'preload-sandbox.js') + + // http protocol to simulate accessing a another domain. this is required + // because the code paths for cross domain popups is different. + function crossDomainHandler (request, callback) { + callback({ + mimeType: 'text/html', + data: `

${request.url}

` + }) + } + + before(function (done) { + protocol.interceptStringProtocol('http', crossDomainHandler, function () { + done() + }) + }) + + after(function (done) { + protocol.uninterceptProtocol('http', function () { + done() + }) + }) + + it('exposes ipcRenderer to preload script', function (done) { + ipcMain.once('answer', function (event, test) { + assert.equal(test, 'preload') + done() + }) + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: preload + } + }) + w.loadURL('file://' + path.join(fixtures, 'api', 'preload.html')) + }) + + it('exposes "exit" event to preload script', function (done) { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: preload + } + }) + let htmlPath = path.join(fixtures, 'api', 'sandbox.html?exit-event') + const pageUrl = 'file://' + htmlPath + w.loadURL(pageUrl) + ipcMain.once('answer', function (event, url) { + let expectedUrl = pageUrl + if (process.platform === 'win32') { + expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') + } + assert.equal(url, expectedUrl) + done() + }) + }) + + it('should open windows in same domain with cross-scripting enabled', function (done) { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: preload + } + }) + let htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open') + const pageUrl = 'file://' + htmlPath + w.loadURL(pageUrl) + w.webContents.once('new-window', (e, url, frameName, disposition, options) => { + let expectedUrl = pageUrl + if (process.platform === 'win32') { + expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') + } + assert.equal(url, expectedUrl) + assert.equal(frameName, 'popup!') + assert.equal(options.x, 50) + assert.equal(options.y, 60) + assert.equal(options.width, 500) + assert.equal(options.height, 600) + ipcMain.once('answer', function (event, html) { + assert.equal(html, '

scripting from opener

') + done() + }) + }) + }) + + it('should open windows in another domain with cross-scripting disabled', function (done) { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: preload + } + }) + let htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open-external') + const pageUrl = 'file://' + htmlPath + w.loadURL(pageUrl) + w.webContents.once('new-window', (e, url, frameName, disposition, options) => { + assert.equal(url, 'http://www.google.com/#q=electron') + assert.equal(options.x, 55) + assert.equal(options.y, 65) + assert.equal(options.width, 505) + assert.equal(options.height, 605) + ipcMain.once('child-loaded', function (event, openerIsNull, html) { + assert(openerIsNull) + assert.equal(html, '

http://www.google.com/#q=electron

') + ipcMain.once('answer', function (event, exceptionMessage) { + assert(/Blocked a frame with origin/.test(exceptionMessage)) + done() + }) + w.webContents.send('child-loaded') + }) + }) + }) + + it('should set ipc event sender correctly', function (done) { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true, + preload: preload + } + }) + let htmlPath = path.join(fixtures, 'api', 'sandbox.html?verify-ipc-sender') + const pageUrl = 'file://' + htmlPath + w.loadURL(pageUrl) + w.webContents.once('new-window', (e, url, frameName, disposition, options) => { + let parentWc = w.webContents + let childWc = options.webContents + assert.notEqual(parentWc, childWc) + ipcMain.once('parent-ready', function (event) { + assert.equal(parentWc, event.sender) + parentWc.send('verified') + }) + ipcMain.once('child-ready', function (event) { + assert.equal(childWc, event.sender) + childWc.send('verified') + }) + waitForEvents(ipcMain, [ + 'parent-answer', + 'child-answer' + ], done) + }) + }) + + describe('event handling', function () { + it('works for window events', function (done) { + waitForEvents(w, [ + 'page-title-updated' + ], done) + w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?window-events')) + }) + + it('works for web contents events', function (done) { + waitForEvents(w.webContents, [ + 'did-navigate', + 'did-fail-load', + 'did-stop-loading' + ], done) + w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?webcontents-stop')) + waitForEvents(w.webContents, [ + 'did-finish-load', + 'did-frame-finish-load', + 'did-navigate-in-page', + 'will-navigate', + 'did-start-loading', + 'did-stop-loading', + 'did-frame-finish-load', + 'dom-ready' + ], done) + w.loadURL('file://' + path.join(fixtures, 'api', 'sandbox.html?webcontents-events')) + }) + }) + }) }) describe('beforeunload handler', function () { diff --git a/spec/fixtures/api/sandbox.html b/spec/fixtures/api/sandbox.html new file mode 100644 index 000000000000..a684664a9665 --- /dev/null +++ b/spec/fixtures/api/sandbox.html @@ -0,0 +1,76 @@ + + + diff --git a/spec/fixtures/module/preload-sandbox.js b/spec/fixtures/module/preload-sandbox.js new file mode 100644 index 000000000000..39a8704e1329 --- /dev/null +++ b/spec/fixtures/module/preload-sandbox.js @@ -0,0 +1,13 @@ +(function () { + const {ipcRenderer} = require('electron') + window.ipcRenderer = ipcRenderer + if (location.protocol === 'file:') { + window.test = 'preload' + window.require = require + window.process = process + } else if (location.href !== 'about:blank') { + addEventListener('DOMContentLoaded', () => { + ipcRenderer.send('child-loaded', window.opener == null, document.body.innerHTML) + }, false) + } +})() diff --git a/tools/js2c.py b/tools/js2c.py index 394aa557e522..ce4081077021 100755 --- a/tools/js2c.py +++ b/tools/js2c.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import contextlib +import glob import os import subprocess import sys @@ -11,7 +12,7 @@ SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) def main(): natives = os.path.abspath(sys.argv[1]) - js_source_files = sys.argv[2:] + js_source_files = glob.glob('{0}/*.js'.format(sys.argv[2])) call_js2c(natives, js_source_files)