feat: session.resolveHost (#37690)

* 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 committed by GitHub
parent db27b9f433
commit 6bfef67aae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 477 additions and 0 deletions

View file

@ -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
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)`
* `url` URL

View file

@ -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`

View file

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

View file

@ -114,6 +114,8 @@ auto_filenames = {
"docs/api/structures/protocol-response.md",
"docs/api/structures/rectangle.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/segmented-control-segment.md",
"docs/api/structures/serial-port.md",

View file

@ -433,6 +433,8 @@ filenames = {
"shell/browser/net/proxying_url_loader_factory.h",
"shell/browser/net/proxying_websocket.cc",
"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.h",
"shell/browser/net/system_network_context_manager.cc",

View file

@ -65,6 +65,7 @@
#include "shell/browser/javascript_environment.h"
#include "shell/browser/media/media_device_id_salt.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/common/gin_converters/callback_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;
}
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() {
gin_helper::Promise<int64_t> promise(isolate_);
auto handle = promise.GetHandle();
@ -1242,6 +1274,7 @@ gin::Handle<Session> Session::New() {
void Session::FillObjectTemplate(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> templ) {
gin::ObjectTemplateBuilder(isolate, "Session", templ)
.SetMethod("resolveHost", &Session::ResolveHost)
.SetMethod("resolveProxy", &Session::ResolveProxy)
.SetMethod("getCacheSize", &Session::GetCacheSize)
.SetMethod("clearCache", &Session::ClearCache)

View file

@ -13,6 +13,7 @@
#include "electron/buildflags/buildflags.h"
#include "gin/handle.h"
#include "gin/wrappable.h"
#include "services/network/public/mojom/host_resolver.mojom.h"
#include "services/network/public/mojom/ssl_config.mojom.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/net/resolve_proxy_helper.h"
@ -96,6 +97,9 @@ class Session : public gin::Wrappable<Session>,
const char* GetTypeName() override;
// 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> GetCacheSize();
v8::Local<v8::Promise> ClearCache();

View file

@ -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

View file

@ -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_

View file

@ -663,4 +663,154 @@ v8::Local<v8::Value> Converter<net::RedirectInfo>::ToV8(
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

42
shell/common/gin_converters/net_converter.h Executable file → Normal file
View file

@ -11,6 +11,7 @@
#include "gin/converter.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 "shell/browser/net/cert_verifier_client.h"
@ -109,6 +110,47 @@ struct Converter<net::RedirectInfo> {
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>
struct Converter<std::vector<std::pair<K, V>>> {
static bool FromV8(v8::Isolate* isolate,

View file

@ -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()', () => {
const scheme = 'cors-blob';
const protocol = session.defaultSession.protocol;

View file

@ -22,6 +22,11 @@ app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
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.zoomScheme = 'zoom';