Commit 82bb9093 authored by bartfab's avatar bartfab Committed by Commit bot

Add an ONC property for the third-party VPN provider extension ID

This CL maps the third-party VPN provider extension ID (which is
stored in shill's |Provider.Host| field) to an ONC property.

BUG=460428
TEST=Extended unit tests and API test

Review URL: https://codereview.chromium.org/1019033002

Cr-Commit-Position: refs/heads/master@{#321238}
parent 07840d89
......@@ -24,12 +24,12 @@ bool VPNProvider::Key::MatchesNetwork(
const chromeos::NetworkState& network) const {
if (network.type() != shill::kTypeVPN)
return false;
if (third_party)
return network.vpn_provider_extension_id() == extension_id;
// Currently, all networks with an empty |vpn_provider_extension_id| use a
// single built-in VPN providers. In the future, we may distinguish between
// multiple built-in providers based on the |Provider.Type| property.
return network.vpn_provider_extension_id().empty();
const bool network_uses_third_party_provider =
network.vpn_provider_type() == shill::kProviderThirdPartyVpn;
if (!third_party)
return !network_uses_third_party_provider;
return network_uses_third_party_provider &&
network.third_party_vpn_provider_extension_id() == extension_id;
}
VPNProvider::VPNProvider(const Key& key, const std::string& name)
......
......@@ -358,6 +358,15 @@ class NetworkingPrivateChromeOSApiTest : public ExtensionApiTest {
base::StringValue(shill::kProviderOpenVpn));
profile_test->AddService(kUser1ProfilePath, "stub_vpn1");
AddService("stub_vpn2", "vpn2", shill::kTypeVPN, shill::kStateOffline);
service_test_->SetServiceProperty(
"stub_vpn2", shill::kProviderTypeProperty,
base::StringValue(shill::kProviderThirdPartyVpn));
service_test_->SetServiceProperty(
"stub_vpn2", shill::kProviderHostProperty,
base::StringValue("third_party_provider_extension_id"));
profile_test->AddService(kUser1ProfilePath, "stub_vpn2");
content::RunAllPendingInMessageLoop();
}
......
......@@ -231,7 +231,7 @@ var availableTests = [
"ConnectionState": "Connected",
"GUID": "stub_wimax_guid",
"Name": "wimax",
"Source":"User",
"Source": "User",
"Type": "WiMAX",
"WiMAX": {
"SignalStrength": 40
......@@ -241,8 +241,24 @@ var availableTests = [
"ConnectionState": "Connected",
"GUID": "stub_vpn1_guid",
"Name": "vpn1",
"Source":"User",
"Type": "VPN"
"Source": "User",
"Type": "VPN",
"VPN": {
"Type":"OpenVPN"
}
},
{
"ConnectionState": "NotConnected",
"GUID": "stub_vpn2_guid",
"Name": "vpn2",
"Source": "User",
"Type": "VPN",
"VPN": {
"ThirdPartyVPN": {
"ExtensionID": "third_party_provider_extension_id"
},
"Type": "ThirdPartyVPN"
}
},
{
"Connectable": true,
......@@ -295,6 +311,7 @@ var availableTests = [
"stub_wifi1_guid",
"stub_wimax_guid",
"stub_vpn1_guid",
"stub_vpn2_guid",
"stub_wifi2_guid"];
var done = chrome.test.callbackAdded();
var listener = new privateHelpers.listListener(expected, done);
......@@ -524,7 +541,8 @@ var availableTests = [
"stub_wifi2_guid",
"stub_wimax_guid",
"stub_vpn1_guid",
"stub_wifi1_guid"];
"stub_wifi1_guid",
"stub_vpn2_guid"];
var done = chrome.test.callbackAdded();
var listener = new privateHelpers.listListener(expected, done);
chrome.networkingPrivate.onNetworkListChanged.addListener(
......
......@@ -4,6 +4,7 @@
#include "chromeos/network/network_state.h"
#include "base/memory/scoped_ptr.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
......@@ -166,25 +167,29 @@ bool NetworkState::PropertyChanged(const std::string& key,
}
return true;
} else if (key == shill::kProviderProperty) {
std::string vpn_provider_type;
const base::DictionaryValue* dict;
std::string provider_type;
if (!value.GetAsDictionary(&dict) ||
!dict->GetStringWithoutPathExpansion(shill::kTypeProperty,
&provider_type)) {
&vpn_provider_type)) {
NET_LOG(ERROR) << "Failed to parse " << path() << "." << key;
return false;
}
if (provider_type != shill::kProviderThirdPartyVpn) {
// If the network uses the built-in OpenVPN and L2TP support, set the
// provider extension ID to an empty string.
vpn_provider_extension_id_.clear();
return true;
if (vpn_provider_type == shill::kProviderThirdPartyVpn) {
// If the network uses a third-party VPN provider, copy over the
// provider's extension ID, which is held in |shill::kHostProperty|.
if (!dict->GetStringWithoutPathExpansion(
shill::kHostProperty, &third_party_vpn_provider_extension_id_)) {
NET_LOG(ERROR) << "Failed to parse " << path() << "." << key;
return false;
}
} else {
third_party_vpn_provider_extension_id_.clear();
}
// If the network uses a third-party VPN provider, copy over the provider's
// extension ID, which is held in |shill::kHostProperty|.
return dict->GetStringWithoutPathExpansion(shill::kHostProperty,
&vpn_provider_extension_id_);
vpn_provider_type_ = vpn_provider_type;
return true;
}
return false;
}
......@@ -232,6 +237,22 @@ void NetworkState::GetStateProperties(base::DictionaryValue* dictionary) const {
connection_state());
}
// VPN properties.
if (NetworkTypePattern::VPN().MatchesType(type())) {
// Shill sends VPN provider properties in a nested dictionary. |dictionary|
// must replicate that nested structure.
scoped_ptr<base::DictionaryValue> provider_property(
new base::DictionaryValue);
provider_property->SetStringWithoutPathExpansion(shill::kTypeProperty,
vpn_provider_type_);
if (vpn_provider_type_ == shill::kProviderThirdPartyVpn) {
provider_property->SetStringWithoutPathExpansion(
shill::kHostProperty, third_party_vpn_provider_extension_id_);
}
dictionary->SetWithoutPathExpansion(shill::kProviderProperty,
provider_property.release());
}
// Wireless properties
if (!NetworkTypePattern::Wireless().MatchesType(type()))
return;
......
......@@ -92,8 +92,11 @@ class CHROMEOS_EXPORT NetworkState : public ManagedState {
const std::string& roaming() const { return roaming_; }
const std::string& payment_url() const { return payment_url_; }
bool cellular_out_of_credits() const { return cellular_out_of_credits_; }
const std::string& vpn_provider_extension_id() const {
return vpn_provider_extension_id_;
// VPN property accessors
const std::string& vpn_provider_type() const { return vpn_provider_type_; }
const std::string& third_party_vpn_provider_extension_id() const {
return third_party_vpn_provider_extension_id_;
}
// Returns true if |connection_state_| is a connected/connecting state.
......@@ -183,10 +186,10 @@ class CHROMEOS_EXPORT NetworkState : public ManagedState {
std::string payment_url_;
bool cellular_out_of_credits_;
// VPN property. For networks using a third-party VPN provider, this will be
// the provider's extension ID. For networks using the built-in OpenVPN and
// L2TP support, this will be an empty string.
std::string vpn_provider_extension_id_;
// VPN properties, used to construct the display name and to show the correct
// configuration dialog.
std::string vpn_provider_type_;
std::string third_party_vpn_provider_extension_id_;
// TODO(pneubeck): Remove this once (Managed)NetworkConfigurationHandler
// provides proxy configuration. crbug.com/241775
......
......@@ -54,11 +54,14 @@ class NetworkStateTest : public testing::Test {
}
protected:
bool SetProperty(const std::string& key, scoped_ptr<base::Value> value) {
const bool result = network_state_.PropertyChanged(key, *value);
properties_.SetWithoutPathExpansion(key, value.release());
return result;
}
bool SetStringProperty(const std::string& key, const std::string& value) {
TestStringValue* string_value = new TestStringValue(value);
bool res = network_state_.PropertyChanged(key, *string_value);
properties_.SetWithoutPathExpansion(key, string_value);
return res;
return SetProperty(key, make_scoped_ptr(new TestStringValue(value)));
}
bool SignalInitialPropertiesReceived() {
......@@ -213,4 +216,21 @@ TEST_F(NetworkStateTest, CaptivePortalState) {
EXPECT_TRUE(network_state_.is_captive_portal());
}
// Third-party VPN provider.
TEST_F(NetworkStateTest, VPNThirdPartyProvider) {
EXPECT_TRUE(SetStringProperty(shill::kTypeProperty, shill::kTypeVPN));
EXPECT_TRUE(SetStringProperty(shill::kNameProperty, "VPN"));
scoped_ptr<base::DictionaryValue> provider(new base::DictionaryValue);
provider->SetStringWithoutPathExpansion(shill::kTypeProperty,
shill::kProviderThirdPartyVpn);
provider->SetStringWithoutPathExpansion(
shill::kHostProperty, "third-party-vpn-provider-extension-id");
EXPECT_TRUE(SetProperty(shill::kProviderProperty, provider.Pass()));
SignalInitialPropertiesReceived();
EXPECT_EQ(network_state_.vpn_provider_type(), shill::kProviderThirdPartyVpn);
EXPECT_EQ(network_state_.third_party_vpn_provider_extension_id(),
"third-party-vpn-provider-extension-id");
}
} // namespace chromeos
......@@ -232,6 +232,7 @@ void Normalizer::NormalizeVPN(base::DictionaryValue* vpn) {
RemoveEntryUnless(vpn, kOpenVPN, type == kOpenVPN);
RemoveEntryUnless(vpn, kIPsec, type == kIPsec || type == kTypeL2TP_IPsec);
RemoveEntryUnless(vpn, kL2TP, type == kTypeL2TP_IPsec);
RemoveEntryUnless(vpn, kThirdPartyVpn, type == kThirdPartyVpn);
}
void Normalizer::NormalizeWiFi(base::DictionaryValue* wifi) {
......
......@@ -151,6 +151,11 @@ const OncFieldSignature openvpn_fields[] = {
{ ::onc::openvpn::kVerifyX509, &kVerifyX509Signature},
{NULL}};
const OncFieldSignature third_party_vpn_fields[] = {
{ ::onc::kRecommended, &kRecommendedSignature},
{ ::onc::third_party_vpn::kExtensionID, &kStringSignature},
{NULL}};
const OncFieldSignature verify_x509_fields[] = {
{ ::onc::verify_x509::kName, &kStringSignature},
{ ::onc::verify_x509::kType, &kStringSignature},
......@@ -163,6 +168,7 @@ const OncFieldSignature vpn_fields[] = {
{ ::onc::vpn::kIPsec, &kIPsecSignature},
{ ::onc::vpn::kL2TP, &kL2TPSignature},
{ ::onc::vpn::kOpenVPN, &kOpenVPNSignature},
{ ::onc::vpn::kThirdPartyVpn, &kThirdPartyVPNSignature},
{ ::onc::vpn::kType, &kStringSignature},
{NULL}};
......@@ -385,6 +391,9 @@ const OncValueSignature kL2TPSignature = {
const OncValueSignature kOpenVPNSignature = {
base::Value::TYPE_DICTIONARY, openvpn_fields, NULL
};
const OncValueSignature kThirdPartyVPNSignature = {
base::Value::TYPE_DICTIONARY, third_party_vpn_fields, NULL
};
const OncValueSignature kVerifyX509Signature = {
base::Value::TYPE_DICTIONARY, verify_x509_fields, NULL
};
......
......@@ -43,6 +43,7 @@ CHROMEOS_EXPORT extern const OncValueSignature kIPsecSignature;
CHROMEOS_EXPORT extern const OncValueSignature kL2TPSignature;
CHROMEOS_EXPORT extern const OncValueSignature kXAUTHSignature;
CHROMEOS_EXPORT extern const OncValueSignature kOpenVPNSignature;
CHROMEOS_EXPORT extern const OncValueSignature kThirdPartyVPNSignature;
CHROMEOS_EXPORT extern const OncValueSignature kVerifyX509Signature;
CHROMEOS_EXPORT extern const OncValueSignature kVPNSignature;
CHROMEOS_EXPORT extern const OncValueSignature kEthernetSignature;
......
......@@ -188,10 +188,28 @@ void LocalTranslator::TranslateIPsec() {
}
void LocalTranslator::TranslateVPN() {
CopyFieldFromONCToShill(::onc::vpn::kHost, shill::kProviderHostProperty);
std::string type;
if (onc_object_->GetStringWithoutPathExpansion(::onc::vpn::kType, &type))
TranslateWithTableAndSet(type, kVPNTypeTable, shill::kProviderTypeProperty);
std::string onc_type;
if (onc_object_->GetStringWithoutPathExpansion(::onc::vpn::kType,
&onc_type)) {
TranslateWithTableAndSet(onc_type, kVPNTypeTable,
shill::kProviderTypeProperty);
}
if (onc_type == ::onc::vpn::kThirdPartyVpn) {
// For third-party VPNs, |shill::kProviderHostProperty| is used to store the
// provider's extension ID.
const base::DictionaryValue* onc_third_party_vpn = nullptr;
onc_object_->GetDictionaryWithoutPathExpansion(::onc::vpn::kThirdPartyVpn,
&onc_third_party_vpn);
std::string onc_extension_id;
if (onc_third_party_vpn &&
onc_third_party_vpn->GetStringWithoutPathExpansion(
::onc::third_party_vpn::kExtensionID, &onc_extension_id)) {
shill_dictionary_->SetStringWithoutPathExpansion(
shill::kProviderHostProperty, onc_extension_id);
}
} else {
CopyFieldFromONCToShill(::onc::vpn::kHost, shill::kProviderHostProperty);
}
CopyFieldsAccordingToSignature();
}
......
......@@ -77,6 +77,7 @@ class ShillToONCTranslator {
void TranslateEthernet();
void TranslateOpenVPN();
void TranslateIPsec();
void TranslateThirdPartyVPN();
void TranslateVPN();
void TranslateWiFiWithState();
void TranslateWiMAXWithState();
......@@ -159,6 +160,8 @@ ShillToONCTranslator::CreateTranslatedONCObject() {
TranslateOpenVPN();
} else if (onc_signature_ == &kIPsecSignature) {
TranslateIPsec();
} else if (onc_signature_ == &kThirdPartyVPNSignature) {
TranslateThirdPartyVPN();
} else if (onc_signature_ == &kWiFiWithStateSignature) {
TranslateWiFiWithState();
} else if (onc_signature_ == &kWiMAXWithStateSignature) {
......@@ -266,6 +269,18 @@ void ShillToONCTranslator::TranslateIPsec() {
authentication_type);
}
void ShillToONCTranslator::TranslateThirdPartyVPN() {
CopyPropertiesAccordingToSignature();
// For third-party VPNs, |shill::kProviderHostProperty| is used to store the
// provider's extension ID.
std::string shill_extension_id;
shill_dictionary_->GetStringWithoutPathExpansion(shill::kHostProperty,
&shill_extension_id);
onc_object_->SetStringWithoutPathExpansion(
::onc::third_party_vpn::kExtensionID, shill_extension_id);
}
void ShillToONCTranslator::TranslateVPN() {
CopyPropertiesAccordingToSignature();
......@@ -285,11 +300,12 @@ void ShillToONCTranslator::TranslateVPN() {
}
onc_object_->SetStringWithoutPathExpansion(::onc::vpn::kType,
onc_provider_type);
std::string provider_host;
if (provider->GetStringWithoutPathExpansion(shill::kHostProperty,
&provider_host)) {
std::string shill_provider_host;
if (onc_provider_type != ::onc::vpn::kThirdPartyVpn &&
provider->GetStringWithoutPathExpansion(shill::kHostProperty,
&shill_provider_host)) {
onc_object_->SetStringWithoutPathExpansion(::onc::vpn::kHost,
provider_host);
shill_provider_host);
}
// Translate the nested dictionary.
......@@ -298,13 +314,14 @@ void ShillToONCTranslator::TranslateVPN() {
TranslateAndAddNestedObject(::onc::vpn::kIPsec, *provider);
TranslateAndAddNestedObject(::onc::vpn::kL2TP, *provider);
provider_type_dictionary = ::onc::vpn::kIPsec;
} else if (onc_provider_type != ::onc::vpn::kThirdPartyVpn) {
} else {
TranslateAndAddNestedObject(onc_provider_type, *provider);
provider_type_dictionary = onc_provider_type;
}
bool save_credentials;
if (shill_dictionary_->GetBooleanWithoutPathExpansion(
if (onc_provider_type != ::onc::vpn::kThirdPartyVpn &&
shill_dictionary_->GetBooleanWithoutPathExpansion(
shill::kSaveCredentialsProperty, &save_credentials)) {
SetNestedOncValue(provider_type_dictionary,
::onc::vpn::kSaveCredentials,
......
......@@ -61,7 +61,8 @@ INSTANTIATE_TEST_CASE_P(
std::make_pair("openvpn_clientcert_with_cert_pems.onc",
"shill_openvpn_clientcert.json"),
std::make_pair("cellular.onc", "shill_cellular.json"),
std::make_pair("wimax.onc", "shill_wimax.json")));
std::make_pair("wimax.onc", "shill_wimax.json"),
std::make_pair("third_party_vpn.onc", "shill_third_party_vpn.json")));
// First parameter: Filename of source Shill json.
// Second parameter: Filename of expected translated ONC network part.
......@@ -113,7 +114,9 @@ INSTANTIATE_TEST_CASE_P(
std::make_pair("shill_cellular_with_state.json",
"translation_of_shill_cellular_with_state.onc"),
std::make_pair("shill_wimax_with_state.json",
"translation_of_shill_wimax_with_state.onc")));
"translation_of_shill_wimax_with_state.onc"),
std::make_pair("shill_output_third_party_vpn.json",
"third_party_vpn.onc")));
} // namespace onc
} // namespace chromeos
......@@ -122,6 +122,8 @@ scoped_ptr<base::DictionaryValue> Validator::MapObject(
valid = ValidateIPsec(repaired.get());
} else if (&signature == &kOpenVPNSignature) {
valid = ValidateOpenVPN(repaired.get());
} else if (&signature == &kThirdPartyVPNSignature) {
valid = ValidateThirdPartyVPN(repaired.get());
} else if (&signature == &kVerifyX509Signature) {
valid = ValidateVerifyX509(repaired.get());
} else if (&signature == &kCertificatePatternSignature) {
......@@ -683,7 +685,8 @@ bool Validator::ValidateWiFi(base::DictionaryValue* result) {
bool Validator::ValidateVPN(base::DictionaryValue* result) {
using namespace ::onc::vpn;
const char* const kValidTypes[] = {kIPsec, kTypeL2TP_IPsec, kOpenVPN};
const char* const kValidTypes[] = {
kIPsec, kTypeL2TP_IPsec, kOpenVPN, kThirdPartyVpn};
const std::vector<const char*> valid_types(toVector(kValidTypes));
if (FieldExistsAndHasNoValidValue(*result, ::onc::vpn::kType, valid_types))
return false;
......@@ -698,6 +701,8 @@ bool Validator::ValidateVPN(base::DictionaryValue* result) {
} else if (type == kTypeL2TP_IPsec) {
all_required_exist &=
RequireField(*result, kIPsec) && RequireField(*result, kL2TP);
} else if (type == kThirdPartyVpn) {
all_required_exist &= RequireField(*result, kThirdPartyVpn);
}
return !error_on_missing_field_ || all_required_exist;
......@@ -802,6 +807,13 @@ bool Validator::ValidateOpenVPN(base::DictionaryValue* result) {
return !error_on_missing_field_ || all_required_exist;
}
bool Validator::ValidateThirdPartyVPN(base::DictionaryValue* result) {
const bool all_required_exist =
RequireField(*result, ::onc::third_party_vpn::kExtensionID);
return !error_on_missing_field_ || all_required_exist;
}
bool Validator::ValidateVerifyX509(base::DictionaryValue* result) {
using namespace ::onc::verify_x509;
......
......@@ -160,6 +160,7 @@ class CHROMEOS_EXPORT Validator : public Mapper {
bool ValidateVPN(base::DictionaryValue* result);
bool ValidateIPsec(base::DictionaryValue* result);
bool ValidateOpenVPN(base::DictionaryValue* result);
bool ValidateThirdPartyVPN(base::DictionaryValue* result);
bool ValidateVerifyX509(base::DictionaryValue* result);
bool ValidateCertificatePattern(base::DictionaryValue* result);
bool ValidateProxySettings(base::DictionaryValue* result);
......
......@@ -207,6 +207,9 @@ INSTANTIATE_TEST_CASE_P(
&kNetworkConfigurationSignature,
false),
OncParams("openvpn_with_password.onc",
&kNetworkConfigurationSignature,
false),
OncParams("third_party_vpn.onc",
&kNetworkConfigurationSignature,
false)));
......@@ -348,6 +351,10 @@ INSTANTIATE_TEST_CASE_P(
std::make_pair(OncParams("openvpn-missing-verify-x509-name",
&kNetworkConfigurationSignature,
false),
ExpectStrictNotValid("")),
std::make_pair(OncParams("third-party-vpn-missing-extension-id",
&kNetworkConfigurationSignature,
false),
ExpectStrictNotValid(""))));
// Strict validator returns INVALID. Liberal validator repairs.
......
......@@ -396,6 +396,15 @@
}
}
},
"third-party-vpn-missing-extension-id": {
"GUID": "guid",
"Name": "third-party VPN",
"Type": "VPN",
"VPN": {
"Type": "ThirdPartyVPN",
"ThirdPartyVPN": { }
}
},
"toplevel-empty": {
"Type": "UnencryptedConfiguration",
"NetworkConfigurations": [ ]
......
{ "GUID": "guid",
"Type": "vpn",
"Name": "third-party VPN",
"Provider": {
"Host": "deadbeefdeadbeefdeadbeefdeadbeef",
"Type": "thirdpartyvpn",
},
}
{ "GUID": "guid",
"Type": "vpn",
"Name": "third-party VPN",
"Provider.Host": "deadbeefdeadbeefdeadbeefdeadbeef",
"Provider.Type": "thirdpartyvpn",
}
{
"GUID": "guid",
"Name": "third-party VPN",
"Type": "VPN",
"VPN": {
"Type": "ThirdPartyVPN",
"ThirdPartyVPN": {
"ExtensionID": "deadbeefdeadbeefdeadbeefdeadbeef",
}
}
}
......@@ -850,6 +850,16 @@
OpenVPN settings.
</dd>
<dt class="field">ThirdPartyVPN</dt>
<dd>
<span class="field_meta">
(required if <span class="field">Type</span> is
<span class="value">ThirdPartyVPN</span>, otherwise ignored)
<span class="type">ThirdPartyVPN</span>
</span>
Third-party VPN provider settings.
</dd>
<dt class="field">Type</dt>
<dd>
<span class="field_meta">
......@@ -859,8 +869,9 @@
<span class="rule">
<span class="rule_id"></span>
Allowed values are <span class="value">IPsec</span>,
<span class="value">L2TP-IPsec</span>, and
<span class="value">OpenVPN</span>.
<span class="value">L2TP-IPsec</span>,
<span class="value">OpenVPN</span>, and
<span class="value">ThirdPartyVPN</span>.
</span>
Type of the VPN.
</dd>
......@@ -1580,7 +1591,29 @@
Determines which of the host's X.509 names will be verified. Allowed values are <span class="value">name</span>, <span class="value">name-prefix</span> and <span class="value">subject</span>. See OpenVPN's documentation for "--verify-x509-name" for the meaning of each value. Defaults to OpenVPN's default if not specified.
</dd>
</dl>
</section>
<section>
<h1>Third-party VPN provider based connections and types</h1>
<p>
<span class="field">VPN.Type</span> must be
<span class="value">ThirdPartyVPN</span>.
</p>
<p>
<span class="type">ThirdPartyVPN</span> type contains the following:
</p>
<dl class="field_list">
<dt class="field">ExtensionID</dt>
<dd>
<span class="field_meta">
(required)
<span class="type">string</span>
</span>
The extension ID of the third-party VPN provider used by this network.
</dd>
</dl>
</section>
</section>
......
......@@ -349,6 +349,10 @@ const char kPassword[] = "Password";
const char kPasswordAndOTP[] = "PasswordAndOTP";
} // openvpn_user_auth_type
namespace third_party_vpn {
const char kExtensionID[] = "ExtensionID";
} // third_party_vpn
namespace verify_x509 {
const char kName[] = "Name";
const char kType[] = "Type";
......
......@@ -367,6 +367,10 @@ ONC_EXPORT extern const char kPassword[];
ONC_EXPORT extern const char kPasswordAndOTP[];
} // openvpn_user_auth_type
namespace third_party_vpn {
ONC_EXPORT extern const char kExtensionID[];
} // third_party_vpn
namespace verify_x509 {
ONC_EXPORT extern const char kName[];
ONC_EXPORT extern const char kType[];
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment