Merge pull request #3487 from atom/certificate-api
Rework of the certificate API
This commit is contained in:
commit
45e3d4ea6b
15 changed files with 256 additions and 225 deletions
|
@ -17,6 +17,7 @@
|
||||||
#include "atom/common/native_mate_converters/callback.h"
|
#include "atom/common/native_mate_converters/callback.h"
|
||||||
#include "atom/common/native_mate_converters/net_converter.h"
|
#include "atom/common/native_mate_converters/net_converter.h"
|
||||||
#include "atom/common/native_mate_converters/file_path_converter.h"
|
#include "atom/common/native_mate_converters/file_path_converter.h"
|
||||||
|
#include "atom/common/native_mate_converters/gurl_converter.h"
|
||||||
#include "atom/common/node_includes.h"
|
#include "atom/common/node_includes.h"
|
||||||
#include "atom/common/options_switches.h"
|
#include "atom/common/options_switches.h"
|
||||||
#include "base/command_line.h"
|
#include "base/command_line.h"
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
#include "chrome/common/chrome_paths.h"
|
#include "chrome/common/chrome_paths.h"
|
||||||
#include "content/public/browser/client_certificate_delegate.h"
|
#include "content/public/browser/client_certificate_delegate.h"
|
||||||
#include "content/public/browser/gpu_data_manager.h"
|
#include "content/public/browser/gpu_data_manager.h"
|
||||||
|
#include "content/public/browser/render_frame_host.h"
|
||||||
#include "content/public/common/content_switches.h"
|
#include "content/public/common/content_switches.h"
|
||||||
#include "native_mate/dictionary.h"
|
#include "native_mate/dictionary.h"
|
||||||
#include "native_mate/object_template_builder.h"
|
#include "native_mate/object_template_builder.h"
|
||||||
|
@ -154,11 +156,14 @@ void PassLoginInformation(scoped_refptr<LoginHandler> login_handler,
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
App::App() {
|
App::App() {
|
||||||
|
static_cast<AtomBrowserClient*>(AtomBrowserClient::Get())->set_delegate(this);
|
||||||
Browser::Get()->AddObserver(this);
|
Browser::Get()->AddObserver(this);
|
||||||
content::GpuDataManager::GetInstance()->AddObserver(this);
|
content::GpuDataManager::GetInstance()->AddObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
App::~App() {
|
App::~App() {
|
||||||
|
static_cast<AtomBrowserClient*>(AtomBrowserClient::Get())->set_delegate(
|
||||||
|
nullptr);
|
||||||
Browser::Get()->RemoveObserver(this);
|
Browser::Get()->RemoveObserver(this);
|
||||||
content::GpuDataManager::GetInstance()->RemoveObserver(this);
|
content::GpuDataManager::GetInstance()->RemoveObserver(this);
|
||||||
}
|
}
|
||||||
|
@ -212,15 +217,59 @@ void App::OnFinishLaunching() {
|
||||||
Emit("ready");
|
Emit("ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::OnSelectCertificate(
|
void App::OnLogin(LoginHandler* login_handler) {
|
||||||
|
v8::Locker locker(isolate());
|
||||||
|
v8::HandleScope handle_scope(isolate());
|
||||||
|
bool prevent_default = Emit(
|
||||||
|
"login",
|
||||||
|
WebContents::CreateFrom(isolate(), login_handler->GetWebContents()),
|
||||||
|
login_handler->request(),
|
||||||
|
login_handler->auth_info(),
|
||||||
|
base::Bind(&PassLoginInformation, make_scoped_refptr(login_handler)));
|
||||||
|
|
||||||
|
// Default behavior is to always cancel the auth.
|
||||||
|
if (!prevent_default)
|
||||||
|
login_handler->CancelAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::AllowCertificateError(
|
||||||
|
int pid,
|
||||||
|
int fid,
|
||||||
|
int cert_error,
|
||||||
|
const net::SSLInfo& ssl_info,
|
||||||
|
const GURL& request_url,
|
||||||
|
content::ResourceType resource_type,
|
||||||
|
bool overridable,
|
||||||
|
bool strict_enforcement,
|
||||||
|
bool expired_previous_decision,
|
||||||
|
const base::Callback<void(bool)>& callback,
|
||||||
|
content::CertificateRequestResultType* request) {
|
||||||
|
auto rfh = content::RenderFrameHost::FromID(pid, fid);
|
||||||
|
auto web_contents = content::WebContents::FromRenderFrameHost(rfh);
|
||||||
|
|
||||||
|
v8::Locker locker(isolate());
|
||||||
|
v8::HandleScope handle_scope(isolate());
|
||||||
|
bool prevent_default = Emit("certificate-error",
|
||||||
|
WebContents::CreateFrom(isolate(), web_contents),
|
||||||
|
request_url,
|
||||||
|
net::ErrorToString(cert_error),
|
||||||
|
ssl_info.cert,
|
||||||
|
callback);
|
||||||
|
|
||||||
|
// Deny the certificate by default.
|
||||||
|
if (!prevent_default)
|
||||||
|
*request = content::CERTIFICATE_REQUEST_RESULT_TYPE_DENY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::SelectClientCertificate(
|
||||||
content::WebContents* web_contents,
|
content::WebContents* web_contents,
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
net::SSLCertRequestInfo* cert_request_info,
|
||||||
scoped_ptr<content::ClientCertificateDelegate> delegate) {
|
scoped_ptr<content::ClientCertificateDelegate> delegate) {
|
||||||
std::shared_ptr<content::ClientCertificateDelegate>
|
std::shared_ptr<content::ClientCertificateDelegate>
|
||||||
shared_delegate(delegate.release());
|
shared_delegate(delegate.release());
|
||||||
bool prevent_default =
|
bool prevent_default =
|
||||||
Emit("select-certificate",
|
Emit("select-client-certificate",
|
||||||
api::WebContents::CreateFrom(isolate(), web_contents),
|
WebContents::CreateFrom(isolate(), web_contents),
|
||||||
cert_request_info->host_and_port.ToString(),
|
cert_request_info->host_and_port.ToString(),
|
||||||
cert_request_info->client_certs,
|
cert_request_info->client_certs,
|
||||||
base::Bind(&OnClientCertificateSelected,
|
base::Bind(&OnClientCertificateSelected,
|
||||||
|
@ -233,31 +282,6 @@ void App::OnSelectCertificate(
|
||||||
cert_request_info->client_certs[0].get());
|
cert_request_info->client_certs[0].get());
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::OnLogin(LoginHandler* login_handler) {
|
|
||||||
// Convert the args explicitly since they will be passed for twice.
|
|
||||||
v8::Locker locker(isolate());
|
|
||||||
v8::HandleScope handle_scope(isolate());
|
|
||||||
auto web_contents =
|
|
||||||
WebContents::CreateFrom(isolate(), login_handler->GetWebContents());
|
|
||||||
auto request = mate::ConvertToV8(isolate(), login_handler->request());
|
|
||||||
auto auth_info = mate::ConvertToV8(isolate(), login_handler->auth_info());
|
|
||||||
auto callback = mate::ConvertToV8(
|
|
||||||
isolate(),
|
|
||||||
base::Bind(&PassLoginInformation, make_scoped_refptr(login_handler)));
|
|
||||||
|
|
||||||
bool prevent_default =
|
|
||||||
Emit("login", web_contents, request, auth_info, callback);
|
|
||||||
|
|
||||||
// Also pass it to WebContents.
|
|
||||||
if (!prevent_default)
|
|
||||||
prevent_default =
|
|
||||||
web_contents->Emit("login", request, auth_info, callback);
|
|
||||||
|
|
||||||
// Default behavior is to always cancel the auth.
|
|
||||||
if (!prevent_default)
|
|
||||||
login_handler->CancelAuth();
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::OnGpuProcessCrashed(base::TerminationStatus exit_code) {
|
void App::OnGpuProcessCrashed(base::TerminationStatus exit_code) {
|
||||||
Emit("gpu-process-crashed");
|
Emit("gpu-process-crashed");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "atom/browser/api/event_emitter.h"
|
#include "atom/browser/api/event_emitter.h"
|
||||||
|
#include "atom/browser/atom_browser_client.h"
|
||||||
#include "atom/browser/browser_observer.h"
|
#include "atom/browser/browser_observer.h"
|
||||||
#include "atom/common/native_mate_converters/callback.h"
|
#include "atom/common/native_mate_converters/callback.h"
|
||||||
#include "chrome/browser/process_singleton.h"
|
#include "chrome/browser/process_singleton.h"
|
||||||
|
@ -26,7 +27,8 @@ namespace atom {
|
||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
class App : public mate::EventEmitter,
|
class App : public AtomBrowserClient::Delegate,
|
||||||
|
public mate::EventEmitter,
|
||||||
public BrowserObserver,
|
public BrowserObserver,
|
||||||
public content::GpuDataManagerObserver {
|
public content::GpuDataManagerObserver {
|
||||||
public:
|
public:
|
||||||
|
@ -46,11 +48,25 @@ class App : public mate::EventEmitter,
|
||||||
void OnActivate(bool has_visible_windows) override;
|
void OnActivate(bool has_visible_windows) override;
|
||||||
void OnWillFinishLaunching() override;
|
void OnWillFinishLaunching() override;
|
||||||
void OnFinishLaunching() override;
|
void OnFinishLaunching() override;
|
||||||
void OnSelectCertificate(
|
void OnLogin(LoginHandler* login_handler) override;
|
||||||
|
|
||||||
|
// content::ContentBrowserClient:
|
||||||
|
void AllowCertificateError(
|
||||||
|
int render_process_id,
|
||||||
|
int render_frame_id,
|
||||||
|
int cert_error,
|
||||||
|
const net::SSLInfo& ssl_info,
|
||||||
|
const GURL& request_url,
|
||||||
|
content::ResourceType resource_type,
|
||||||
|
bool overridable,
|
||||||
|
bool strict_enforcement,
|
||||||
|
bool expired_previous_decision,
|
||||||
|
const base::Callback<void(bool)>& callback,
|
||||||
|
content::CertificateRequestResultType* request) override;
|
||||||
|
void SelectClientCertificate(
|
||||||
content::WebContents* web_contents,
|
content::WebContents* web_contents,
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
net::SSLCertRequestInfo* cert_request_info,
|
||||||
scoped_ptr<content::ClientCertificateDelegate> delegate) override;
|
scoped_ptr<content::ClientCertificateDelegate> delegate) override;
|
||||||
void OnLogin(LoginHandler* login_handler) override;
|
|
||||||
|
|
||||||
// content::GpuDataManagerObserver:
|
// content::GpuDataManagerObserver:
|
||||||
void OnGpuProcessCrashed(base::TerminationStatus exit_code) override;
|
void OnGpuProcessCrashed(base::TerminationStatus exit_code) override;
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include "atom/browser/api/save_page_handler.h"
|
#include "atom/browser/api/save_page_handler.h"
|
||||||
#include "atom/browser/atom_browser_context.h"
|
#include "atom/browser/atom_browser_context.h"
|
||||||
#include "atom/browser/atom_browser_main_parts.h"
|
#include "atom/browser/atom_browser_main_parts.h"
|
||||||
|
#include "atom/browser/net/atom_cert_verifier.h"
|
||||||
#include "atom/common/native_mate_converters/callback.h"
|
#include "atom/common/native_mate_converters/callback.h"
|
||||||
#include "atom/common/native_mate_converters/gurl_converter.h"
|
#include "atom/common/native_mate_converters/gurl_converter.h"
|
||||||
#include "atom/common/native_mate_converters/file_path_converter.h"
|
#include "atom/common/native_mate_converters/file_path_converter.h"
|
||||||
|
@ -238,18 +239,11 @@ void SetProxyInIO(net::URLRequestContextGetter* getter,
|
||||||
RunCallbackInUI(callback);
|
RunCallbackInUI(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PassVerificationResult(
|
|
||||||
scoped_refptr<AtomCertVerifier::CertVerifyRequest> request,
|
|
||||||
bool success) {
|
|
||||||
request->ContinueWithResult(success ? net::OK : net::ERR_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Session::Session(AtomBrowserContext* browser_context)
|
Session::Session(AtomBrowserContext* browser_context)
|
||||||
: browser_context_(browser_context) {
|
: browser_context_(browser_context) {
|
||||||
AttachAsUserData(browser_context);
|
AttachAsUserData(browser_context);
|
||||||
browser_context->cert_verifier()->SetDelegate(this);
|
|
||||||
|
|
||||||
// Observe DownloadManger to get download notifications.
|
// Observe DownloadManger to get download notifications.
|
||||||
content::BrowserContext::GetDownloadManager(browser_context)->
|
content::BrowserContext::GetDownloadManager(browser_context)->
|
||||||
|
@ -262,19 +256,6 @@ Session::~Session() {
|
||||||
Destroy();
|
Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::RequestCertVerification(
|
|
||||||
const scoped_refptr<AtomCertVerifier::CertVerifyRequest>& request) {
|
|
||||||
bool prevent_default = Emit(
|
|
||||||
"untrusted-certificate",
|
|
||||||
request->args().hostname,
|
|
||||||
request->args().cert,
|
|
||||||
base::Bind(&PassVerificationResult, request));
|
|
||||||
|
|
||||||
if (!prevent_default)
|
|
||||||
// Tell the request to use the result of default verifier.
|
|
||||||
request->ContinueWithResult(net::ERR_IO_PENDING);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::OnDownloadCreated(content::DownloadManager* manager,
|
void Session::OnDownloadCreated(content::DownloadManager* manager,
|
||||||
content::DownloadItem* item) {
|
content::DownloadItem* item) {
|
||||||
auto web_contents = item->GetWebContents();
|
auto web_contents = item->GetWebContents();
|
||||||
|
@ -295,7 +276,6 @@ bool Session::IsDestroyed() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::Destroy() {
|
void Session::Destroy() {
|
||||||
browser_context_->cert_verifier()->SetDelegate(nullptr);
|
|
||||||
browser_context_ = nullptr;
|
browser_context_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,6 +357,17 @@ void Session::DisableNetworkEmulation() {
|
||||||
base::Passed(&conditions)));
|
base::Passed(&conditions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Session::SetCertVerifyProc(v8::Local<v8::Value> val,
|
||||||
|
mate::Arguments* args) {
|
||||||
|
AtomCertVerifier::VerifyProc proc;
|
||||||
|
if (!(val->IsNull() || mate::ConvertFromV8(args->isolate(), val, &proc))) {
|
||||||
|
args->ThrowError("Must pass null or function");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browser_context_->cert_verifier()->SetVerifyProc(proc);
|
||||||
|
}
|
||||||
|
|
||||||
v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
|
v8::Local<v8::Value> Session::Cookies(v8::Isolate* isolate) {
|
||||||
if (cookies_.IsEmpty()) {
|
if (cookies_.IsEmpty()) {
|
||||||
auto handle = atom::api::Cookies::Create(isolate, browser_context());
|
auto handle = atom::api::Cookies::Create(isolate, browser_context());
|
||||||
|
@ -395,6 +386,7 @@ mate::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
||||||
.SetMethod("setDownloadPath", &Session::SetDownloadPath)
|
.SetMethod("setDownloadPath", &Session::SetDownloadPath)
|
||||||
.SetMethod("enableNetworkEmulation", &Session::EnableNetworkEmulation)
|
.SetMethod("enableNetworkEmulation", &Session::EnableNetworkEmulation)
|
||||||
.SetMethod("disableNetworkEmulation", &Session::DisableNetworkEmulation)
|
.SetMethod("disableNetworkEmulation", &Session::DisableNetworkEmulation)
|
||||||
|
.SetMethod("setCertificateVerifyProc", &Session::SetCertVerifyProc)
|
||||||
.SetProperty("cookies", &Session::Cookies);
|
.SetProperty("cookies", &Session::Cookies);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "atom/browser/api/trackable_object.h"
|
#include "atom/browser/api/trackable_object.h"
|
||||||
#include "atom/browser/net/atom_cert_verifier.h"
|
|
||||||
#include "content/public/browser/download_manager.h"
|
#include "content/public/browser/download_manager.h"
|
||||||
#include "native_mate/handle.h"
|
#include "native_mate/handle.h"
|
||||||
#include "net/base/completion_callback.h"
|
#include "net/base/completion_callback.h"
|
||||||
|
@ -35,7 +34,6 @@ class AtomBrowserContext;
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
class Session: public mate::TrackableObject<Session>,
|
class Session: public mate::TrackableObject<Session>,
|
||||||
public AtomCertVerifier::Delegate,
|
|
||||||
public content::DownloadManager::Observer {
|
public content::DownloadManager::Observer {
|
||||||
public:
|
public:
|
||||||
using ResolveProxyCallback = base::Callback<void(std::string)>;
|
using ResolveProxyCallback = base::Callback<void(std::string)>;
|
||||||
|
@ -54,10 +52,6 @@ class Session: public mate::TrackableObject<Session>,
|
||||||
explicit Session(AtomBrowserContext* browser_context);
|
explicit Session(AtomBrowserContext* browser_context);
|
||||||
~Session();
|
~Session();
|
||||||
|
|
||||||
// AtomCertVerifier::Delegate:
|
|
||||||
void RequestCertVerification(
|
|
||||||
const scoped_refptr<AtomCertVerifier::CertVerifyRequest>&) override;
|
|
||||||
|
|
||||||
// content::DownloadManager::Observer:
|
// content::DownloadManager::Observer:
|
||||||
void OnDownloadCreated(content::DownloadManager* manager,
|
void OnDownloadCreated(content::DownloadManager* manager,
|
||||||
content::DownloadItem* item) override;
|
content::DownloadItem* item) override;
|
||||||
|
@ -78,6 +72,7 @@ class Session: public mate::TrackableObject<Session>,
|
||||||
void SetDownloadPath(const base::FilePath& path);
|
void SetDownloadPath(const base::FilePath& path);
|
||||||
void EnableNetworkEmulation(const mate::Dictionary& options);
|
void EnableNetworkEmulation(const mate::Dictionary& options);
|
||||||
void DisableNetworkEmulation();
|
void DisableNetworkEmulation();
|
||||||
|
void SetCertVerifyProc(v8::Local<v8::Value> proc, mate::Arguments* args);
|
||||||
v8::Local<v8::Value> Cookies(v8::Isolate* isolate);
|
v8::Local<v8::Value> Cookies(v8::Isolate* isolate);
|
||||||
|
|
||||||
// Cached object for cookies API.
|
// Cached object for cookies API.
|
||||||
|
|
|
@ -38,6 +38,12 @@ app.getAppPath = ->
|
||||||
# Helpers.
|
# Helpers.
|
||||||
app.resolveProxy = (url, callback) -> @defaultSession.resolveProxy url, callback
|
app.resolveProxy = (url, callback) -> @defaultSession.resolveProxy url, callback
|
||||||
|
|
||||||
|
# Routes the events to webContents.
|
||||||
|
for name in ['login', 'certificate-error', 'select-client-certificate']
|
||||||
|
do (name) ->
|
||||||
|
app.on name, (event, webContents, args...) ->
|
||||||
|
webContents.emit name, event, args...
|
||||||
|
|
||||||
# Deprecated.
|
# Deprecated.
|
||||||
{deprecate} = electron
|
{deprecate} = electron
|
||||||
app.getHomeDir = deprecate 'app.getHomeDir', 'app.getPath', ->
|
app.getHomeDir = deprecate 'app.getHomeDir', 'app.getPath', ->
|
||||||
|
@ -52,6 +58,7 @@ deprecate.event app, 'finish-launching', 'ready', ->
|
||||||
@emit 'finish-launching'
|
@emit 'finish-launching'
|
||||||
deprecate.event app, 'activate-with-no-open-windows', 'activate', (event, hasVisibleWindows) ->
|
deprecate.event app, 'activate-with-no-open-windows', 'activate', (event, hasVisibleWindows) ->
|
||||||
@emit 'activate-with-no-open-windows' if not hasVisibleWindows
|
@emit 'activate-with-no-open-windows' if not hasVisibleWindows
|
||||||
|
deprecate.event app, 'select-certificate', 'select-client-certificate'
|
||||||
|
|
||||||
# Wrappers for native classes.
|
# Wrappers for native classes.
|
||||||
wrapSession = (session) ->
|
wrapSession = (session) ->
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
#include "atom/browser/atom_quota_permission_context.h"
|
#include "atom/browser/atom_quota_permission_context.h"
|
||||||
#include "atom/browser/atom_resource_dispatcher_host_delegate.h"
|
#include "atom/browser/atom_resource_dispatcher_host_delegate.h"
|
||||||
#include "atom/browser/atom_speech_recognition_manager_delegate.h"
|
#include "atom/browser/atom_speech_recognition_manager_delegate.h"
|
||||||
#include "atom/browser/browser.h"
|
|
||||||
#include "atom/browser/native_window.h"
|
#include "atom/browser/native_window.h"
|
||||||
#include "atom/browser/web_contents_preferences.h"
|
#include "atom/browser/web_contents_preferences.h"
|
||||||
#include "atom/browser/window_list.h"
|
#include "atom/browser/window_list.h"
|
||||||
|
@ -88,7 +87,7 @@ void AtomBrowserClient::SetCustomSchemes(
|
||||||
g_custom_schemes = JoinString(schemes, ',');
|
g_custom_schemes = JoinString(schemes, ',');
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomBrowserClient::AtomBrowserClient() {
|
AtomBrowserClient::AtomBrowserClient() : delegate_(nullptr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomBrowserClient::~AtomBrowserClient() {
|
AtomBrowserClient::~AtomBrowserClient() {
|
||||||
|
@ -208,6 +207,26 @@ content::QuotaPermissionContext*
|
||||||
return new AtomQuotaPermissionContext;
|
return new AtomQuotaPermissionContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AtomBrowserClient::AllowCertificateError(
|
||||||
|
int render_process_id,
|
||||||
|
int render_frame_id,
|
||||||
|
int cert_error,
|
||||||
|
const net::SSLInfo& ssl_info,
|
||||||
|
const GURL& request_url,
|
||||||
|
content::ResourceType resource_type,
|
||||||
|
bool overridable,
|
||||||
|
bool strict_enforcement,
|
||||||
|
bool expired_previous_decision,
|
||||||
|
const base::Callback<void(bool)>& callback,
|
||||||
|
content::CertificateRequestResultType* request) {
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->AllowCertificateError(
|
||||||
|
render_process_id, render_frame_id, cert_error, ssl_info, request_url,
|
||||||
|
resource_type, overridable, strict_enforcement,
|
||||||
|
expired_previous_decision, callback, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AtomBrowserClient::SelectClientCertificate(
|
void AtomBrowserClient::SelectClientCertificate(
|
||||||
content::WebContents* web_contents,
|
content::WebContents* web_contents,
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
net::SSLCertRequestInfo* cert_request_info,
|
||||||
|
@ -222,10 +241,10 @@ void AtomBrowserClient::SelectClientCertificate(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cert_request_info->client_certs.empty())
|
if (!cert_request_info->client_certs.empty() && delegate_) {
|
||||||
Browser::Get()->ClientCertificateSelector(web_contents,
|
delegate_->SelectClientCertificate(
|
||||||
cert_request_info,
|
web_contents, cert_request_info, delegate.Pass());
|
||||||
delegate.Pass());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AtomBrowserClient::ResourceDispatcherHostCreated() {
|
void AtomBrowserClient::ResourceDispatcherHostCreated() {
|
||||||
|
|
|
@ -31,6 +31,9 @@ class AtomBrowserClient : public brightray::BrowserClient,
|
||||||
AtomBrowserClient();
|
AtomBrowserClient();
|
||||||
virtual ~AtomBrowserClient();
|
virtual ~AtomBrowserClient();
|
||||||
|
|
||||||
|
using Delegate = content::ContentBrowserClient;
|
||||||
|
void set_delegate(Delegate* delegate) { delegate_ = delegate; }
|
||||||
|
|
||||||
// Don't force renderer process to restart for once.
|
// Don't force renderer process to restart for once.
|
||||||
static void SuppressRendererProcessRestartForOnce();
|
static void SuppressRendererProcessRestartForOnce();
|
||||||
// Custom schemes to be registered to standard.
|
// Custom schemes to be registered to standard.
|
||||||
|
@ -54,6 +57,18 @@ class AtomBrowserClient : public brightray::BrowserClient,
|
||||||
int child_process_id) override;
|
int child_process_id) override;
|
||||||
void DidCreatePpapiPlugin(content::BrowserPpapiHost* browser_host) override;
|
void DidCreatePpapiPlugin(content::BrowserPpapiHost* browser_host) override;
|
||||||
content::QuotaPermissionContext* CreateQuotaPermissionContext() override;
|
content::QuotaPermissionContext* CreateQuotaPermissionContext() override;
|
||||||
|
void AllowCertificateError(
|
||||||
|
int render_process_id,
|
||||||
|
int render_frame_id,
|
||||||
|
int cert_error,
|
||||||
|
const net::SSLInfo& ssl_info,
|
||||||
|
const GURL& request_url,
|
||||||
|
content::ResourceType resource_type,
|
||||||
|
bool overridable,
|
||||||
|
bool strict_enforcement,
|
||||||
|
bool expired_previous_decision,
|
||||||
|
const base::Callback<void(bool)>& callback,
|
||||||
|
content::CertificateRequestResultType* request) override;
|
||||||
void SelectClientCertificate(
|
void SelectClientCertificate(
|
||||||
content::WebContents* web_contents,
|
content::WebContents* web_contents,
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
net::SSLCertRequestInfo* cert_request_info,
|
||||||
|
@ -74,6 +89,8 @@ class AtomBrowserClient : public brightray::BrowserClient,
|
||||||
scoped_ptr<AtomResourceDispatcherHostDelegate>
|
scoped_ptr<AtomResourceDispatcherHostDelegate>
|
||||||
resource_dispatcher_host_delegate_;
|
resource_dispatcher_host_delegate_;
|
||||||
|
|
||||||
|
Delegate* delegate_;
|
||||||
|
|
||||||
DISALLOW_COPY_AND_ASSIGN(AtomBrowserClient);
|
DISALLOW_COPY_AND_ASSIGN(AtomBrowserClient);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@
|
||||||
#include "atom/browser/native_window.h"
|
#include "atom/browser/native_window.h"
|
||||||
#include "atom/browser/window_list.h"
|
#include "atom/browser/window_list.h"
|
||||||
#include "base/message_loop/message_loop.h"
|
#include "base/message_loop/message_loop.h"
|
||||||
#include "content/public/browser/client_certificate_delegate.h"
|
|
||||||
#include "net/ssl/ssl_cert_request_info.h"
|
|
||||||
|
|
||||||
namespace atom {
|
namespace atom {
|
||||||
|
|
||||||
|
@ -141,17 +139,6 @@ void Browser::DidFinishLaunching() {
|
||||||
FOR_EACH_OBSERVER(BrowserObserver, observers_, OnFinishLaunching());
|
FOR_EACH_OBSERVER(BrowserObserver, observers_, OnFinishLaunching());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Browser::ClientCertificateSelector(
|
|
||||||
content::WebContents* web_contents,
|
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
|
||||||
scoped_ptr<content::ClientCertificateDelegate> delegate) {
|
|
||||||
FOR_EACH_OBSERVER(BrowserObserver,
|
|
||||||
observers_,
|
|
||||||
OnSelectCertificate(web_contents,
|
|
||||||
cert_request_info,
|
|
||||||
delegate.Pass()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Browser::RequestLogin(LoginHandler* login_handler) {
|
void Browser::RequestLogin(LoginHandler* login_handler) {
|
||||||
FOR_EACH_OBSERVER(BrowserObserver, observers_, OnLogin(login_handler));
|
FOR_EACH_OBSERVER(BrowserObserver, observers_, OnLogin(login_handler));
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,12 +126,6 @@ class Browser : public WindowListObserver {
|
||||||
void WillFinishLaunching();
|
void WillFinishLaunching();
|
||||||
void DidFinishLaunching();
|
void DidFinishLaunching();
|
||||||
|
|
||||||
// Called when client certificate is required.
|
|
||||||
void ClientCertificateSelector(
|
|
||||||
content::WebContents* web_contents,
|
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
|
||||||
scoped_ptr<content::ClientCertificateDelegate> delegate);
|
|
||||||
|
|
||||||
// Request basic auth login.
|
// Request basic auth login.
|
||||||
void RequestLogin(LoginHandler* login_handler);
|
void RequestLogin(LoginHandler* login_handler);
|
||||||
|
|
||||||
|
|
|
@ -7,17 +7,6 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "base/memory/scoped_ptr.h"
|
|
||||||
#include "content/public/browser/client_certificate_delegate.h"
|
|
||||||
|
|
||||||
namespace content {
|
|
||||||
class WebContents;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace net {
|
|
||||||
class SSLCertRequestInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace atom {
|
namespace atom {
|
||||||
|
|
||||||
class LoginHandler;
|
class LoginHandler;
|
||||||
|
@ -53,12 +42,6 @@ class BrowserObserver {
|
||||||
virtual void OnWillFinishLaunching() {}
|
virtual void OnWillFinishLaunching() {}
|
||||||
virtual void OnFinishLaunching() {}
|
virtual void OnFinishLaunching() {}
|
||||||
|
|
||||||
// The browser requires client certificate.
|
|
||||||
virtual void OnSelectCertificate(
|
|
||||||
content::WebContents* web_contents,
|
|
||||||
net::SSLCertRequestInfo* cert_request_info,
|
|
||||||
scoped_ptr<content::ClientCertificateDelegate> delegate) {}
|
|
||||||
|
|
||||||
// The browser requests HTTP login.
|
// The browser requests HTTP login.
|
||||||
virtual void OnLogin(LoginHandler* login_handler) {}
|
virtual void OnLogin(LoginHandler* login_handler) {}
|
||||||
|
|
||||||
|
|
|
@ -15,30 +15,31 @@ using content::BrowserThread;
|
||||||
|
|
||||||
namespace atom {
|
namespace atom {
|
||||||
|
|
||||||
AtomCertVerifier::CertVerifyRequest::~CertVerifyRequest() {
|
namespace {
|
||||||
}
|
|
||||||
|
|
||||||
void AtomCertVerifier::CertVerifyRequest::ContinueWithResult(int result) {
|
void OnResult(
|
||||||
DCHECK_CURRENTLY_ON(BrowserThread::UI);
|
net::CertVerifyResult* verify_result,
|
||||||
|
const net::CompletionCallback& callback,
|
||||||
if (handled_)
|
bool result) {
|
||||||
return;
|
|
||||||
|
|
||||||
handled_ = true;
|
|
||||||
BrowserThread::PostTask(
|
BrowserThread::PostTask(
|
||||||
BrowserThread::IO, FROM_HERE,
|
BrowserThread::IO, FROM_HERE,
|
||||||
base::Bind(args_.callback,
|
base::Bind(callback, result ? net::OK : net::ERR_FAILED));
|
||||||
result == net::ERR_IO_PENDING ? result_ : result));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
AtomCertVerifier::AtomCertVerifier()
|
AtomCertVerifier::AtomCertVerifier()
|
||||||
: delegate_(nullptr) {
|
: default_cert_verifier_(net::CertVerifier::CreateDefault()) {
|
||||||
default_cert_verifier_.reset(net::CertVerifier::CreateDefault());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomCertVerifier::~AtomCertVerifier() {
|
AtomCertVerifier::~AtomCertVerifier() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AtomCertVerifier::SetVerifyProc(const VerifyProc& proc) {
|
||||||
|
base::AutoLock auto_lock(lock_);
|
||||||
|
verify_proc_ = proc;
|
||||||
|
}
|
||||||
|
|
||||||
int AtomCertVerifier::Verify(
|
int AtomCertVerifier::Verify(
|
||||||
net::X509Certificate* cert,
|
net::X509Certificate* cert,
|
||||||
const std::string& hostname,
|
const std::string& hostname,
|
||||||
|
@ -51,45 +52,26 @@ int AtomCertVerifier::Verify(
|
||||||
const net::BoundNetLog& net_log) {
|
const net::BoundNetLog& net_log) {
|
||||||
DCHECK_CURRENTLY_ON(BrowserThread::IO);
|
DCHECK_CURRENTLY_ON(BrowserThread::IO);
|
||||||
|
|
||||||
if (callback.is_null() || !verify_result || hostname.empty() || !delegate_)
|
VerifyProc proc;
|
||||||
return net::ERR_INVALID_ARGUMENT;
|
{
|
||||||
|
base::AutoLock auto_lock(lock_);
|
||||||
VerifyArgs args = { cert, hostname, callback };
|
proc = verify_proc_;
|
||||||
int result = default_cert_verifier_->Verify(
|
|
||||||
cert, hostname, ocsp_response, flags, crl_set, verify_result,
|
|
||||||
base::Bind(&AtomCertVerifier::OnDefaultVerificationResult,
|
|
||||||
base::Unretained(this), args),
|
|
||||||
out_req, net_log);
|
|
||||||
if (result != net::OK && result != net::ERR_IO_PENDING) {
|
|
||||||
// The default verifier fails immediately.
|
|
||||||
VerifyCertificateFromDelegate(args, result);
|
|
||||||
return net::ERR_IO_PENDING;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
if (proc.is_null())
|
||||||
|
return default_cert_verifier_->Verify(
|
||||||
|
cert, hostname, ocsp_response, flags, crl_set, verify_result, callback,
|
||||||
|
out_req, net_log);
|
||||||
|
|
||||||
|
BrowserThread::PostTask(
|
||||||
|
BrowserThread::UI, FROM_HERE,
|
||||||
|
base::Bind(proc, hostname, make_scoped_refptr(cert),
|
||||||
|
base::Bind(OnResult, verify_result, callback)));
|
||||||
|
return net::ERR_IO_PENDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AtomCertVerifier::SupportsOCSPStapling() {
|
bool AtomCertVerifier::SupportsOCSPStapling() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AtomCertVerifier::VerifyCertificateFromDelegate(
|
|
||||||
const VerifyArgs& args, int result) {
|
|
||||||
CertVerifyRequest* request = new CertVerifyRequest(this, result, args);
|
|
||||||
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
|
|
||||||
base::Bind(&Delegate::RequestCertVerification,
|
|
||||||
base::Unretained(delegate_),
|
|
||||||
make_scoped_refptr(request)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AtomCertVerifier::OnDefaultVerificationResult(
|
|
||||||
const VerifyArgs& args, int result) {
|
|
||||||
if (result == net::OK) {
|
|
||||||
args.callback.Run(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VerifyCertificateFromDelegate(args, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace atom
|
} // namespace atom
|
||||||
|
|
|
@ -8,61 +8,22 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "base/memory/ref_counted.h"
|
#include "base/memory/ref_counted.h"
|
||||||
|
#include "base/synchronization/lock.h"
|
||||||
#include "net/cert/cert_verifier.h"
|
#include "net/cert/cert_verifier.h"
|
||||||
|
|
||||||
namespace atom {
|
namespace atom {
|
||||||
|
|
||||||
class AtomCertVerifier : public net::CertVerifier {
|
class AtomCertVerifier : public net::CertVerifier {
|
||||||
public:
|
public:
|
||||||
struct VerifyArgs {
|
|
||||||
scoped_refptr<net::X509Certificate> cert;
|
|
||||||
const std::string& hostname;
|
|
||||||
net::CompletionCallback callback;
|
|
||||||
};
|
|
||||||
|
|
||||||
class CertVerifyRequest
|
|
||||||
: public base::RefCountedThreadSafe<CertVerifyRequest> {
|
|
||||||
public:
|
|
||||||
CertVerifyRequest(AtomCertVerifier* cert_verifier,
|
|
||||||
int result,
|
|
||||||
const VerifyArgs& args)
|
|
||||||
: cert_verifier_(cert_verifier),
|
|
||||||
result_(result),
|
|
||||||
args_(args),
|
|
||||||
handled_(false) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void ContinueWithResult(int result);
|
|
||||||
|
|
||||||
const VerifyArgs& args() const { return args_; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class base::RefCountedThreadSafe<CertVerifyRequest>;
|
|
||||||
~CertVerifyRequest();
|
|
||||||
|
|
||||||
AtomCertVerifier* cert_verifier_;
|
|
||||||
int result_;
|
|
||||||
VerifyArgs args_;
|
|
||||||
bool handled_;
|
|
||||||
|
|
||||||
DISALLOW_COPY_AND_ASSIGN(CertVerifyRequest);
|
|
||||||
};
|
|
||||||
|
|
||||||
class Delegate {
|
|
||||||
public:
|
|
||||||
virtual ~Delegate() {}
|
|
||||||
|
|
||||||
// Called on UI thread.
|
|
||||||
virtual void RequestCertVerification(
|
|
||||||
const scoped_refptr<CertVerifyRequest>& request) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
AtomCertVerifier();
|
AtomCertVerifier();
|
||||||
virtual ~AtomCertVerifier();
|
virtual ~AtomCertVerifier();
|
||||||
|
|
||||||
void SetDelegate(Delegate* delegate) {
|
using VerifyProc =
|
||||||
delegate_ = delegate;
|
base::Callback<void(const std::string& hostname,
|
||||||
}
|
scoped_refptr<net::X509Certificate>,
|
||||||
|
const base::Callback<void(bool)>&)>;
|
||||||
|
|
||||||
|
void SetVerifyProc(const VerifyProc& proc);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// net::CertVerifier:
|
// net::CertVerifier:
|
||||||
|
@ -78,12 +39,8 @@ class AtomCertVerifier : public net::CertVerifier {
|
||||||
bool SupportsOCSPStapling() override;
|
bool SupportsOCSPStapling() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class CertVerifyRequest;
|
base::Lock lock_;
|
||||||
|
VerifyProc verify_proc_;
|
||||||
void VerifyCertificateFromDelegate(const VerifyArgs& args, int result);
|
|
||||||
void OnDefaultVerificationResult(const VerifyArgs& args, int result);
|
|
||||||
|
|
||||||
Delegate* delegate_;
|
|
||||||
scoped_ptr<net::CertVerifier> default_cert_verifier_;
|
scoped_ptr<net::CertVerifier> default_cert_verifier_;
|
||||||
|
|
||||||
DISALLOW_COPY_AND_ASSIGN(AtomCertVerifier);
|
DISALLOW_COPY_AND_ASSIGN(AtomCertVerifier);
|
||||||
|
|
|
@ -131,7 +131,36 @@ Returns:
|
||||||
|
|
||||||
Emitted when a new [browserWindow](browser-window.md) is created.
|
Emitted when a new [browserWindow](browser-window.md) is created.
|
||||||
|
|
||||||
### Event: 'select-certificate'
|
### Event: 'certificate-error'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* `event` Event
|
||||||
|
* `webContents` [WebContents](web-contents.md)
|
||||||
|
* `url` URL
|
||||||
|
* `error` String - The error code
|
||||||
|
* `certificate` Object
|
||||||
|
* `data` Buffer - PEM encoded data
|
||||||
|
* `issuerName` String
|
||||||
|
* `callback` Function
|
||||||
|
|
||||||
|
Emitted when failed to verify the `certificate` for `url`, to trust the
|
||||||
|
certificate you should prevent the default behavior with
|
||||||
|
`event.preventDefault()` and call `callback(true)`.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
session.on('certificate-error', function(event, webContents, url, error, certificate, callback) {
|
||||||
|
if (url == "https://github.com") {
|
||||||
|
// Verification logic.
|
||||||
|
event.preventDefault();
|
||||||
|
callback(true);
|
||||||
|
} else {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event: 'select-client-certificate'
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
|
@ -151,7 +180,7 @@ and `callback` needs to be called with an entry filtered from the list. Using
|
||||||
certificate from the store.
|
certificate from the store.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
app.on('select-certificate', function(event, host, url, list, callback) {
|
app.on('select-client-certificate', function(event, webContents, url, list, callback) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
callback(list[0]);
|
callback(list[0]);
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,31 +34,6 @@ session.on('will-download', function(event, item, webContents) {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Event: 'untrusted-certificate'
|
|
||||||
|
|
||||||
* `event` Event
|
|
||||||
* `hostname` String
|
|
||||||
* `certificate` Object
|
|
||||||
* `data` Buffer - PEM encoded data
|
|
||||||
* `issuerName` String
|
|
||||||
* `callback` Function
|
|
||||||
|
|
||||||
Emitted when failed to verify the `certificate` for `hostname`, to trust the
|
|
||||||
certificate you should prevent the default behavior with
|
|
||||||
`event.preventDefault()` and call `callback(true)`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
session.on('verify-certificate', function(event, hostname, certificate, callback) {
|
|
||||||
if (hostname == "github.com") {
|
|
||||||
// Verification logic.
|
|
||||||
event.preventDefault();
|
|
||||||
callback(true);
|
|
||||||
} else {
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
The `session` object has the following methods:
|
The `session` object has the following methods:
|
||||||
|
@ -245,3 +220,24 @@ window.webContents.session.enableNetworkEmulation({offline: true});
|
||||||
|
|
||||||
Disables any network emulation already active for the `session`. Resets to
|
Disables any network emulation already active for the `session`. Resets to
|
||||||
the original network configuration.
|
the original network configuration.
|
||||||
|
|
||||||
|
### `session.setCertificateVerifyProc(proc)`
|
||||||
|
|
||||||
|
* `proc` Function
|
||||||
|
|
||||||
|
Sets the certificate verify proc for `session`, the `proc` will be called with
|
||||||
|
`proc(hostname, certificate, callback)` whenever a server certificate
|
||||||
|
verification is requested. Calling `callback(true)` accepts the certificate,
|
||||||
|
calling `callback(false)` rejects it.
|
||||||
|
|
||||||
|
Calling `setCertificateVerifyProc(null)` will revert back to default certificate
|
||||||
|
verify proc.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
myWindow.webContents.session.setCertificateVerifyProc(function(hostname, cert, callback) {
|
||||||
|
if (hostname == 'github.com')
|
||||||
|
callback(true);
|
||||||
|
else
|
||||||
|
callback(false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
|
@ -167,6 +167,39 @@ Emitted when DevTools is closed.
|
||||||
|
|
||||||
Emitted when DevTools is focused / opened.
|
Emitted when DevTools is focused / opened.
|
||||||
|
|
||||||
|
### Event: 'certificate-error'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* `event` Event
|
||||||
|
* `url` URL
|
||||||
|
* `error` String - The error code
|
||||||
|
* `certificate` Object
|
||||||
|
* `data` Buffer - PEM encoded data
|
||||||
|
* `issuerName` String
|
||||||
|
* `callback` Function
|
||||||
|
|
||||||
|
Emitted when failed to verify the `certificate` for `url`.
|
||||||
|
|
||||||
|
The usage is the same with [the `certificate-error` event of
|
||||||
|
`app`](app.md#event-certificate-error).
|
||||||
|
|
||||||
|
### Event: 'select-client-certificate'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* `event` Event
|
||||||
|
* `url` URL
|
||||||
|
* `certificateList` [Objects]
|
||||||
|
* `data` Buffer - PEM encoded data
|
||||||
|
* `issuerName` String - Issuer's Common Name
|
||||||
|
* `callback` Function
|
||||||
|
|
||||||
|
Emitted when a client certificate is requested.
|
||||||
|
|
||||||
|
The usage is the same with [the `select-client-certificate` event of
|
||||||
|
`app`](app.md#event-select-client-certificate).
|
||||||
|
|
||||||
### Event: 'login'
|
### Event: 'login'
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue