* feat: session.resolveHost

Expose Chromium's host resolution API through the Session object.

* Update shell/browser/api/electron_api_session.cc

Co-authored-by: Jeremy Rose <nornagon@nornagon.net>

* address feedback

* fix tests

* address feedback

* Add options

* Update shell/browser/api/electron_api_session.cc

Co-authored-by: Cheng Zhao <github@zcbenz.com>

* Update shell/browser/net/resolve_host_function.cc

Co-authored-by: Cheng Zhao <github@zcbenz.com>

* lint

* return object

* add missing file

* fix crash

* handle scope

* links

---------

Co-authored-by: Fedor Indutny <indutny@signal.org>
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
Co-authored-by: Jeremy Rose <nornagon@nornagon.net>
Co-authored-by: Cheng Zhao <github@zcbenz.com>
This commit is contained in:
Fedor Indutny 2023-04-05 07:06:14 -07:00 коммит произвёл GitHub
Родитель db27b9f433
Коммит 6bfef67aae
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 477 добавлений и 0 удалений

Просмотреть файл

@ -690,6 +690,41 @@ The `proxyBypassRules` is a comma separated list of rules described below:
Match local addresses. The meaning of `<local>` is whether the Match local addresses. The meaning of `<local>` is whether the
host matches one of: "127.0.0.1", "::1", "localhost". host matches one of: "127.0.0.1", "::1", "localhost".
#### `ses.resolveHost(host, [options])`
* `host` string - Hostname to resolve.
* `options` Object (optional)
* `queryType` string (optional) - Requested DNS query type. If unspecified,
resolver will pick A or AAAA (or both) based on IPv4/IPv6 settings:
* `A` - Fetch only A records
* `AAAA` - Fetch only AAAA records.
* `source` string (optional) - The source to use for resolved addresses.
Default allows the resolver to pick an appropriate source. Only affects use
of big external sources (e.g. calling the system for resolution or using
DNS). Even if a source is specified, results can still come from cache,
resolving "localhost" or IP literals, etc. One of the following values:
* `any` (default) - Resolver will pick an appropriate source. Results could
come from DNS, MulticastDNS, HOSTS file, etc
* `system` - Results will only be retrieved from the system or OS, e.g. via
the `getaddrinfo()` system call
* `dns` - Results will only come from DNS queries
* `mdns` - Results will only come from Multicast DNS queries
* `localOnly` - No external sources will be used. Results will only come
from fast local sources that are available no matter the source setting,
e.g. cache, hosts file, IP literal resolution, etc.
* `cacheUsage` string (optional) - Indicates what DNS cache entries, if any,
can be used to provide a response. One of the following values:
* `allowed` (default) - Results may come from the host cache if non-stale
* `staleAllowed` - Results may come from the host cache even if stale (by
expiration or network changes)
* `disallowed` - Results will not come from the host cache.
* `secureDnsPolicy` string (optional) - Controls the resolver's Secure DNS
behavior for this request. One of the following values:
* `allow` (default)
* `disable`
Returns [`Promise<ResolvedHost>`](structures/resolved-host.md) - Resolves with the resolved IP addresses for the `host`.
#### `ses.resolveProxy(url)` #### `ses.resolveProxy(url)`
* `url` URL * `url` URL

Просмотреть файл

@ -0,0 +1,7 @@
# ResolvedEndpoint Object
* `address` string
* `family` string - One of the following:
* `ipv4` - Corresponds to `AF_INET`
* `ipv6` - Corresponds to `AF_INET6`
* `unspec` - Corresponds to `AF_UNSPEC`

Просмотреть файл

@ -0,0 +1,3 @@
# ResolvedHost Object
* `endpoints` [ResolvedEndpoint[]](resolved-endpoint.md) - resolved DNS entries for the hostname

Просмотреть файл

@ -114,6 +114,8 @@ auto_filenames = {
"docs/api/structures/protocol-response.md", "docs/api/structures/protocol-response.md",
"docs/api/structures/rectangle.md", "docs/api/structures/rectangle.md",
"docs/api/structures/referrer.md", "docs/api/structures/referrer.md",
"docs/api/structures/resolved-endpoint.md",
"docs/api/structures/resolved-host.md",
"docs/api/structures/scrubber-item.md", "docs/api/structures/scrubber-item.md",
"docs/api/structures/segmented-control-segment.md", "docs/api/structures/segmented-control-segment.md",
"docs/api/structures/serial-port.md", "docs/api/structures/serial-port.md",

Просмотреть файл

@ -433,6 +433,8 @@ filenames = {
"shell/browser/net/proxying_url_loader_factory.h", "shell/browser/net/proxying_url_loader_factory.h",
"shell/browser/net/proxying_websocket.cc", "shell/browser/net/proxying_websocket.cc",
"shell/browser/net/proxying_websocket.h", "shell/browser/net/proxying_websocket.h",
"shell/browser/net/resolve_host_function.cc",
"shell/browser/net/resolve_host_function.h",
"shell/browser/net/resolve_proxy_helper.cc", "shell/browser/net/resolve_proxy_helper.cc",
"shell/browser/net/resolve_proxy_helper.h", "shell/browser/net/resolve_proxy_helper.h",
"shell/browser/net/system_network_context_manager.cc", "shell/browser/net/system_network_context_manager.cc",

Просмотреть файл

@ -65,6 +65,7 @@
#include "shell/browser/javascript_environment.h" #include "shell/browser/javascript_environment.h"
#include "shell/browser/media/media_device_id_salt.h" #include "shell/browser/media/media_device_id_salt.h"
#include "shell/browser/net/cert_verifier_client.h" #include "shell/browser/net/cert_verifier_client.h"
#include "shell/browser/net/resolve_host_function.h"
#include "shell/browser/session_preferences.h" #include "shell/browser/session_preferences.h"
#include "shell/common/gin_converters/callback_converter.h" #include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/content_converter.h" #include "shell/common/gin_converters/content_converter.h"
@ -426,6 +427,37 @@ v8::Local<v8::Promise> Session::ResolveProxy(gin::Arguments* args) {
return handle; return handle;
} }
v8::Local<v8::Promise> Session::ResolveHost(
std::string host,
absl::optional<network::mojom::ResolveHostParametersPtr> params) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate_);
v8::Local<v8::Promise> handle = promise.GetHandle();
auto fn = base::MakeRefCounted<ResolveHostFunction>(
browser_context_, std::move(host),
params ? std::move(params.value()) : nullptr,
base::BindOnce(
[](gin_helper::Promise<gin_helper::Dictionary> promise,
int64_t net_error, const absl::optional<net::AddressList>& addrs) {
if (net_error < 0) {
promise.RejectWithErrorMessage(net::ErrorToString(net_error));
} else {
DCHECK(addrs.has_value() && !addrs->empty());
v8::HandleScope handle_scope(promise.isolate());
gin_helper::Dictionary dict =
gin::Dictionary::CreateEmpty(promise.isolate());
dict.Set("endpoints", addrs->endpoints());
promise.Resolve(dict);
}
},
std::move(promise)));
fn->Run();
return handle;
}
v8::Local<v8::Promise> Session::GetCacheSize() { v8::Local<v8::Promise> Session::GetCacheSize() {
gin_helper::Promise<int64_t> promise(isolate_); gin_helper::Promise<int64_t> promise(isolate_);
auto handle = promise.GetHandle(); auto handle = promise.GetHandle();
@ -1242,6 +1274,7 @@ gin::Handle<Session> Session::New() {
void Session::FillObjectTemplate(v8::Isolate* isolate, void Session::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) { v8::Local<v8::ObjectTemplate> templ) {
gin::ObjectTemplateBuilder(isolate, "Session", templ) gin::ObjectTemplateBuilder(isolate, "Session", templ)
.SetMethod("resolveHost", &Session::ResolveHost)
.SetMethod("resolveProxy", &Session::ResolveProxy) .SetMethod("resolveProxy", &Session::ResolveProxy)
.SetMethod("getCacheSize", &Session::GetCacheSize) .SetMethod("getCacheSize", &Session::GetCacheSize)
.SetMethod("clearCache", &Session::ClearCache) .SetMethod("clearCache", &Session::ClearCache)

Просмотреть файл

@ -13,6 +13,7 @@
#include "electron/buildflags/buildflags.h" #include "electron/buildflags/buildflags.h"
#include "gin/handle.h" #include "gin/handle.h"
#include "gin/wrappable.h" #include "gin/wrappable.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/ssl_config.mojom.h" #include "services/network/public/mojom/ssl_config.mojom.h"
#include "shell/browser/event_emitter_mixin.h" #include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/net/resolve_proxy_helper.h" #include "shell/browser/net/resolve_proxy_helper.h"
@ -96,6 +97,9 @@ class Session : public gin::Wrappable<Session>,
const char* GetTypeName() override; const char* GetTypeName() override;
// Methods. // Methods.
v8::Local<v8::Promise> ResolveHost(
std::string host,
absl::optional<network::mojom::ResolveHostParametersPtr> params);
v8::Local<v8::Promise> ResolveProxy(gin::Arguments* args); v8::Local<v8::Promise> ResolveProxy(gin::Arguments* args);
v8::Local<v8::Promise> GetCacheSize(); v8::Local<v8::Promise> GetCacheSize();
v8::Local<v8::Promise> ClearCache(); v8::Local<v8::Promise> ClearCache();

Просмотреть файл

@ -0,0 +1,78 @@
// Copyright (c) 2023 Signal Messenger, LLC
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/net/resolve_host_function.h"
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/values.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/host_port_pair.h"
#include "net/base/net_errors.h"
#include "net/base/network_isolation_key.h"
#include "net/dns/public/resolve_error_info.h"
#include "shell/browser/electron_browser_context.h"
#include "url/origin.h"
using content::BrowserThread;
namespace electron {
ResolveHostFunction::ResolveHostFunction(
ElectronBrowserContext* browser_context,
std::string host,
network::mojom::ResolveHostParametersPtr params,
ResolveHostCallback callback)
: browser_context_(browser_context),
host_(std::move(host)),
params_(std::move(params)),
callback_(std::move(callback)) {}
ResolveHostFunction::~ResolveHostFunction() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!receiver_.is_bound());
}
void ResolveHostFunction::Run() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!receiver_.is_bound());
// Start the request.
net::HostPortPair host_port_pair(host_, 0);
mojo::PendingRemote<network::mojom::ResolveHostClient> resolve_host_client =
receiver_.BindNewPipeAndPassRemote();
receiver_.set_disconnect_handler(base::BindOnce(
&ResolveHostFunction::OnComplete, this, net::ERR_NAME_NOT_RESOLVED,
net::ResolveErrorInfo(net::ERR_FAILED),
/*resolved_addresses=*/absl::nullopt,
/*endpoint_results_with_metadata=*/absl::nullopt));
browser_context_->GetDefaultStoragePartition()
->GetNetworkContext()
->ResolveHost(network::mojom::HostResolverHost::NewHostPortPair(
std::move(host_port_pair)),
net::NetworkAnonymizationKey(), std::move(params_),
std::move(resolve_host_client));
}
void ResolveHostFunction::OnComplete(
int result,
const net::ResolveErrorInfo& resolve_error_info,
const absl::optional<net::AddressList>& resolved_addresses,
const absl::optional<net::HostResolverEndpointResults>&
endpoint_results_with_metadata) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Ensure that we outlive the `receiver_.reset()` call.
scoped_refptr<ResolveHostFunction> self(this);
receiver_.reset();
std::move(callback_).Run(resolve_error_info.error, resolved_addresses);
}
} // namespace electron

Просмотреть файл

@ -0,0 +1,69 @@
// Copyright (c) 2023 Signal Messenger, LLC
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_NET_RESOLVE_HOST_FUNCTION_H_
#define ELECTRON_SHELL_BROWSER_NET_RESOLVE_HOST_FUNCTION_H_
#include <deque>
#include <string>
#include <vector>
#include "base/memory/ref_counted.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "net/base/address_list.h"
#include "net/dns/public/host_resolver_results.h"
#include "services/network/public/cpp/resolve_host_client_base.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace electron {
class ElectronBrowserContext;
class ResolveHostFunction
: public base::RefCountedThreadSafe<ResolveHostFunction>,
network::ResolveHostClientBase {
public:
using ResolveHostCallback = base::OnceCallback<void(
int64_t,
const absl::optional<net::AddressList>& resolved_addresses)>;
explicit ResolveHostFunction(ElectronBrowserContext* browser_context,
std::string host,
network::mojom::ResolveHostParametersPtr params,
ResolveHostCallback callback);
void Run();
// disable copy
ResolveHostFunction(const ResolveHostFunction&) = delete;
ResolveHostFunction& operator=(const ResolveHostFunction&) = delete;
protected:
~ResolveHostFunction() override;
private:
friend class base::RefCountedThreadSafe<ResolveHostFunction>;
// network::mojom::ResolveHostClient implementation
void OnComplete(int result,
const net::ResolveErrorInfo& resolve_error_info,
const absl::optional<net::AddressList>& resolved_addresses,
const absl::optional<net::HostResolverEndpointResults>&
endpoint_results_with_metadata) override;
// Receiver for the currently in-progress request, if any.
mojo::Receiver<network::mojom::ResolveHostClient> receiver_{this};
// Weak Ref
ElectronBrowserContext* browser_context_;
std::string host_;
network::mojom::ResolveHostParametersPtr params_;
ResolveHostCallback callback_;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NET_RESOLVE_HOST_FUNCTION_H_

Просмотреть файл

@ -663,4 +663,154 @@ v8::Local<v8::Value> Converter<net::RedirectInfo>::ToV8(
return ConvertToV8(isolate, dict); return ConvertToV8(isolate, dict);
} }
// static
v8::Local<v8::Value> Converter<net::IPEndPoint>::ToV8(
v8::Isolate* isolate,
const net::IPEndPoint& val) {
gin::Dictionary dict(isolate, v8::Object::New(isolate));
dict.Set("address", val.ToStringWithoutPort());
switch (val.GetFamily()) {
case net::ADDRESS_FAMILY_IPV4: {
dict.Set("family", "ipv4");
break;
}
case net::ADDRESS_FAMILY_IPV6: {
dict.Set("family", "ipv6");
break;
}
case net::ADDRESS_FAMILY_UNSPECIFIED: {
dict.Set("family", "unspec");
break;
}
}
return ConvertToV8(isolate, dict);
}
// static
bool Converter<net::DnsQueryType>::FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
net::DnsQueryType* out) {
std::string query_type;
if (!ConvertFromV8(isolate, val, &query_type))
return false;
if (query_type == "A") {
*out = net::DnsQueryType::A;
return true;
}
if (query_type == "AAAA") {
*out = net::DnsQueryType::AAAA;
return true;
}
return false;
}
// static
bool Converter<net::HostResolverSource>::FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
net::HostResolverSource* out) {
std::string query_type;
if (!ConvertFromV8(isolate, val, &query_type))
return false;
if (query_type == "any") {
*out = net::HostResolverSource::ANY;
return true;
}
if (query_type == "system") {
*out = net::HostResolverSource::SYSTEM;
return true;
}
if (query_type == "dns") {
*out = net::HostResolverSource::DNS;
return true;
}
if (query_type == "mdns") {
*out = net::HostResolverSource::MULTICAST_DNS;
return true;
}
if (query_type == "localOnly") {
*out = net::HostResolverSource::LOCAL_ONLY;
return true;
}
return false;
}
// static
bool Converter<network::mojom::ResolveHostParameters::CacheUsage>::FromV8(
v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::ResolveHostParameters::CacheUsage* out) {
std::string query_type;
if (!ConvertFromV8(isolate, val, &query_type))
return false;
if (query_type == "allowed") {
*out = network::mojom::ResolveHostParameters::CacheUsage::ALLOWED;
return true;
}
if (query_type == "staleAllowed") {
*out = network::mojom::ResolveHostParameters::CacheUsage::STALE_ALLOWED;
return true;
}
if (query_type == "disallowed") {
*out = network::mojom::ResolveHostParameters::CacheUsage::DISALLOWED;
return true;
}
return false;
}
// static
bool Converter<network::mojom::SecureDnsPolicy>::FromV8(
v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::SecureDnsPolicy* out) {
std::string query_type;
if (!ConvertFromV8(isolate, val, &query_type))
return false;
if (query_type == "allow") {
*out = network::mojom::SecureDnsPolicy::ALLOW;
return true;
}
if (query_type == "disable") {
*out = network::mojom::SecureDnsPolicy::DISABLE;
return true;
}
return false;
}
// static
bool Converter<network::mojom::ResolveHostParametersPtr>::FromV8(
v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::ResolveHostParametersPtr* out) {
gin::Dictionary dict(nullptr);
if (!ConvertFromV8(isolate, val, &dict))
return false;
network::mojom::ResolveHostParametersPtr params =
network::mojom::ResolveHostParameters::New();
dict.Get("queryType", &(params->dns_query_type));
dict.Get("source", &(params->source));
dict.Get("cacheUsage", &(params->cache_usage));
dict.Get("secureDnsPolicy", &(params->secure_dns_policy));
*out = std::move(params);
return true;
}
} // namespace gin } // namespace gin

42
shell/common/gin_converters/net_converter.h Executable file → Normal file
Просмотреть файл

@ -11,6 +11,7 @@
#include "gin/converter.h" #include "gin/converter.h"
#include "services/network/public/mojom/fetch_api.mojom.h" #include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/url_request.mojom.h" #include "services/network/public/mojom/url_request.mojom.h"
#include "shell/browser/net/cert_verifier_client.h" #include "shell/browser/net/cert_verifier_client.h"
@ -109,6 +110,47 @@ struct Converter<net::RedirectInfo> {
const net::RedirectInfo& val); const net::RedirectInfo& val);
}; };
template <>
struct Converter<net::IPEndPoint> {
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
const net::IPEndPoint& val);
};
template <>
struct Converter<net::DnsQueryType> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
net::DnsQueryType* out);
};
template <>
struct Converter<net::HostResolverSource> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
net::HostResolverSource* out);
};
template <>
struct Converter<network::mojom::ResolveHostParameters::CacheUsage> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::ResolveHostParameters::CacheUsage* out);
};
template <>
struct Converter<network::mojom::SecureDnsPolicy> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::SecureDnsPolicy* out);
};
template <>
struct Converter<network::mojom::ResolveHostParametersPtr> {
static bool FromV8(v8::Isolate* isolate,
v8::Local<v8::Value> val,
network::mojom::ResolveHostParametersPtr* out);
};
template <typename K, typename V> template <typename K, typename V>
struct Converter<std::vector<std::pair<K, V>>> { struct Converter<std::vector<std::pair<K, V>>> {
static bool FromV8(v8::Isolate* isolate, static bool FromV8(v8::Isolate* isolate,

Просмотреть файл

@ -487,6 +487,53 @@ describe('session module', () => {
}); });
}); });
describe('ses.resolveHost(host)', () => {
let customSession: Electron.Session;
beforeEach(async () => {
customSession = session.fromPartition('resolvehost');
});
afterEach(() => {
customSession = null as any;
});
it('resolves ipv4.localhost2', async () => {
const { endpoints } = await customSession.resolveHost('ipv4.localhost2');
expect(endpoints).to.be.a('array');
expect(endpoints).to.have.lengthOf(1);
expect(endpoints[0].family).to.equal('ipv4');
expect(endpoints[0].address).to.equal('10.0.0.1');
});
it('fails to resolve AAAA record for ipv4.localhost2', async () => {
await expect(customSession.resolveHost('ipv4.localhost2', {
queryType: 'AAAA'
}))
.to.eventually.be.rejectedWith(/net::ERR_NAME_NOT_RESOLVED/);
});
it('resolves ipv6.localhost2', async () => {
const { endpoints } = await customSession.resolveHost('ipv6.localhost2');
expect(endpoints).to.be.a('array');
expect(endpoints).to.have.lengthOf(1);
expect(endpoints[0].family).to.equal('ipv6');
expect(endpoints[0].address).to.equal('::1');
});
it('fails to resolve A record for ipv6.localhost2', async () => {
await expect(customSession.resolveHost('notfound.localhost2', {
queryType: 'A'
}))
.to.eventually.be.rejectedWith(/net::ERR_NAME_NOT_RESOLVED/);
});
it('fails to resolve notfound.localhost2', async () => {
await expect(customSession.resolveHost('notfound.localhost2'))
.to.eventually.be.rejectedWith(/net::ERR_NAME_NOT_RESOLVED/);
});
});
describe('ses.getBlobData()', () => { describe('ses.getBlobData()', () => {
const scheme = 'cors-blob'; const scheme = 'cors-blob';
const protocol = session.defaultSession.protocol; const protocol = session.defaultSession.protocol;

Просмотреть файл

@ -22,6 +22,11 @@ app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone. // Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream'); app.commandLine.appendSwitch('use-fake-device-for-media-stream');
app.commandLine.appendSwitch('host-rules', 'MAP localhost2 127.0.0.1'); app.commandLine.appendSwitch('host-rules', 'MAP localhost2 127.0.0.1');
app.commandLine.appendSwitch('host-resolver-rules', [
'MAP ipv4.localhost2 10.0.0.1',
'MAP ipv6.localhost2 [::1]',
'MAP notfound.localhost2 ~NOTFOUND'
].join(', '));
global.standardScheme = 'app'; global.standardScheme = 'app';
global.zoomScheme = 'zoom'; global.zoomScheme = 'zoom';