| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 // Implementation of helper functions for the Chrome Extensions Proxy Settings | |
| 6 // API. | |
| 7 // | |
| 8 // Throughout this code, we report errors to the user by setting an |error| | |
| 9 // parameter, if and only if these errors can be cause by invalid input | |
| 10 // from the extension and we cannot expect that the extensions API has | |
| 11 // caught this error before. In all other cases we are dealing with internal | |
| 12 // errors and log to LOG(ERROR). | |
| 13 | |
| 14 #include "chrome/browser/extensions/extension_proxy_api_helpers.h" | |
| 15 | |
| 16 #include "base/base64.h" | |
| 17 #include "base/basictypes.h" | |
| 18 #include "base/string_tokenizer.h" | |
| 19 #include "base/string_util.h" | |
| 20 #include "base/utf_string_conversions.h" | |
| 21 #include "base/values.h" | |
| 22 #include "chrome/browser/extensions/extension_proxy_api_constants.h" | |
| 23 #include "chrome/browser/prefs/proxy_config_dictionary.h" | |
| 24 #include "chrome/common/extensions/extension_error_utils.h" | |
| 25 #include "net/proxy/proxy_config.h" | |
| 26 | |
| 27 namespace keys = extension_proxy_api_constants; | |
| 28 | |
| 29 namespace extension_proxy_api_helpers { | |
| 30 | |
| 31 bool CreateDataURLFromPACScript(const std::string& pac_script, | |
| 32 std::string* pac_script_url_base64_encoded) { | |
| 33 // Encode pac_script in base64. | |
| 34 std::string pac_script_base64_encoded; | |
| 35 if (!base::Base64Encode(pac_script, &pac_script_base64_encoded)) | |
| 36 return false; | |
| 37 | |
| 38 // Make it a correct data url. | |
| 39 *pac_script_url_base64_encoded = | |
| 40 std::string(keys::kPACDataUrlPrefix) + pac_script_base64_encoded; | |
| 41 return true; | |
| 42 } | |
| 43 | |
| 44 bool CreatePACScriptFromDataURL( | |
| 45 const std::string& pac_script_url_base64_encoded, | |
| 46 std::string* pac_script) { | |
| 47 if (pac_script_url_base64_encoded.find(keys::kPACDataUrlPrefix) != 0) | |
| 48 return false; | |
| 49 | |
| 50 // Strip constant data-url prefix. | |
| 51 std::string pac_script_base64_encoded = | |
| 52 pac_script_url_base64_encoded.substr(strlen(keys::kPACDataUrlPrefix)); | |
| 53 | |
| 54 // The rest is a base64 encoded PAC script. | |
| 55 return base::Base64Decode(pac_script_base64_encoded, pac_script); | |
| 56 } | |
| 57 | |
| 58 // Extension Pref -> Browser Pref conversion. | |
| 59 | |
| 60 bool GetProxyModeFromExtensionPref(const DictionaryValue* proxy_config, | |
| 61 ProxyPrefs::ProxyMode* out, | |
| 62 std::string* error, | |
| 63 bool* bad_message) { | |
| 64 std::string proxy_mode; | |
| 65 | |
| 66 // We can safely assume that this is ASCII due to the allowed enumeration | |
| 67 // values specified in the extension API JSON. | |
| 68 proxy_config->GetStringASCII(keys::kProxyConfigMode, &proxy_mode); | |
| 69 if (!ProxyPrefs::StringToProxyMode(proxy_mode, out)) { | |
| 70 LOG(ERROR) << "Invalid mode for proxy settings: " << proxy_mode; | |
| 71 *bad_message = true; | |
| 72 return false; | |
| 73 } | |
| 74 return true; | |
| 75 } | |
| 76 | |
| 77 bool GetPacMandatoryFromExtensionPref(const DictionaryValue* proxy_config, | |
| 78 bool* out, | |
| 79 std::string* error, | |
| 80 bool* bad_message){ | |
| 81 DictionaryValue* pac_dict = NULL; | |
| 82 proxy_config->GetDictionary(keys::kProxyConfigPacScript, &pac_dict); | |
| 83 if (!pac_dict) | |
| 84 return true; | |
| 85 | |
| 86 bool mandatory_pac = false; | |
| 87 if (pac_dict->HasKey(keys::kProxyConfigPacScriptMandatory) && | |
| 88 !pac_dict->GetBoolean(keys::kProxyConfigPacScriptMandatory, | |
| 89 &mandatory_pac)) { | |
| 90 LOG(ERROR) << "'pacScript.mandatory' could not be parsed."; | |
| 91 *bad_message = true; | |
| 92 return false; | |
| 93 } | |
| 94 *out = mandatory_pac; | |
| 95 return true; | |
| 96 } | |
| 97 | |
| 98 bool GetPacUrlFromExtensionPref(const DictionaryValue* proxy_config, | |
| 99 std::string* out, | |
| 100 std::string* error, | |
| 101 bool* bad_message) { | |
| 102 DictionaryValue* pac_dict = NULL; | |
| 103 proxy_config->GetDictionary(keys::kProxyConfigPacScript, &pac_dict); | |
| 104 if (!pac_dict) | |
| 105 return true; | |
| 106 | |
| 107 // TODO(battre): Handle UTF-8 URLs (http://crbug.com/72692). | |
| 108 string16 pac_url16; | |
| 109 if (pac_dict->HasKey(keys::kProxyConfigPacScriptUrl) && | |
| 110 !pac_dict->GetString(keys::kProxyConfigPacScriptUrl, &pac_url16)) { | |
| 111 LOG(ERROR) << "'pacScript.url' could not be parsed."; | |
| 112 *bad_message = true; | |
| 113 return false; | |
| 114 } | |
| 115 if (!IsStringASCII(pac_url16)) { | |
| 116 *error = "'pacScript.url' supports only ASCII URLs " | |
| 117 "(encode URLs in Punycode format)."; | |
| 118 return false; | |
| 119 } | |
| 120 *out = UTF16ToASCII(pac_url16); | |
| 121 return true; | |
| 122 } | |
| 123 | |
| 124 bool GetPacDataFromExtensionPref(const DictionaryValue* proxy_config, | |
| 125 std::string* out, | |
| 126 std::string* error, | |
| 127 bool* bad_message) { | |
| 128 DictionaryValue* pac_dict = NULL; | |
| 129 proxy_config->GetDictionary(keys::kProxyConfigPacScript, &pac_dict); | |
| 130 if (!pac_dict) | |
| 131 return true; | |
| 132 | |
| 133 string16 pac_data16; | |
| 134 if (pac_dict->HasKey(keys::kProxyConfigPacScriptData) && | |
| 135 !pac_dict->GetString(keys::kProxyConfigPacScriptData, &pac_data16)) { | |
| 136 LOG(ERROR) << "'pacScript.data' could not be parsed."; | |
| 137 *bad_message = true; | |
| 138 return false; | |
| 139 } | |
| 140 if (!IsStringASCII(pac_data16)) { | |
| 141 *error = "'pacScript.data' supports only ASCII code" | |
| 142 "(encode URLs in Punycode format)."; | |
| 143 return false; | |
| 144 } | |
| 145 *out = UTF16ToASCII(pac_data16); | |
| 146 return true; | |
| 147 } | |
| 148 | |
| 149 bool GetProxyServer(const DictionaryValue* proxy_server, | |
| 150 net::ProxyServer::Scheme default_scheme, | |
| 151 net::ProxyServer* out, | |
| 152 std::string* error, | |
| 153 bool* bad_message) { | |
| 154 std::string scheme_string; // optional. | |
| 155 | |
| 156 // We can safely assume that this is ASCII due to the allowed enumeration | |
| 157 // values specified in the extension API JSON. | |
| 158 proxy_server->GetStringASCII(keys::kProxyConfigRuleScheme, &scheme_string); | |
| 159 | |
| 160 net::ProxyServer::Scheme scheme = | |
| 161 net::ProxyServer::GetSchemeFromURI(scheme_string); | |
| 162 if (scheme == net::ProxyServer::SCHEME_INVALID) | |
| 163 scheme = default_scheme; | |
| 164 | |
| 165 // TODO(battre): handle UTF-8 in hostnames (http://crbug.com/72692). | |
| 166 string16 host16; | |
| 167 if (!proxy_server->GetString(keys::kProxyConfigRuleHost, &host16)) { | |
| 168 LOG(ERROR) << "Could not parse a 'rules.*.host' entry."; | |
| 169 *bad_message = true; | |
| 170 return false; | |
| 171 } | |
| 172 if (!IsStringASCII(host16)) { | |
| 173 *error = ExtensionErrorUtils::FormatErrorMessage( | |
| 174 "Invalid 'rules.???.host' entry '*'. 'host' field supports only ASCII " | |
| 175 "URLs (encode URLs in Punycode format).", | |
| 176 UTF16ToUTF8(host16)); | |
| 177 return false; | |
| 178 } | |
| 179 std::string host = UTF16ToASCII(host16); | |
| 180 | |
| 181 int port; // optional. | |
| 182 if (!proxy_server->GetInteger(keys::kProxyConfigRulePort, &port)) | |
| 183 port = net::ProxyServer::GetDefaultPortForScheme(scheme); | |
| 184 | |
| 185 *out = net::ProxyServer(scheme, net::HostPortPair(host, port)); | |
| 186 | |
| 187 return true; | |
| 188 } | |
| 189 | |
| 190 bool GetProxyRulesStringFromExtensionPref(const DictionaryValue* proxy_config, | |
| 191 std::string* out, | |
| 192 std::string* error, | |
| 193 bool* bad_message) { | |
| 194 DictionaryValue* proxy_rules = NULL; | |
| 195 proxy_config->GetDictionary(keys::kProxyConfigRules, &proxy_rules); | |
| 196 if (!proxy_rules) | |
| 197 return true; | |
| 198 | |
| 199 // Local data into which the parameters will be parsed. has_proxy describes | |
| 200 // whether a setting was found for the scheme; proxy_server holds the | |
| 201 // respective ProxyServer objects containing those descriptions. | |
| 202 bool has_proxy[keys::SCHEME_MAX + 1]; | |
| 203 net::ProxyServer proxy_server[keys::SCHEME_MAX + 1]; | |
| 204 | |
| 205 // Looking for all possible proxy types is inefficient if we have a | |
| 206 // singleProxy that will supersede per-URL proxies, but it's worth it to keep | |
| 207 // the code simple and extensible. | |
| 208 for (size_t i = 0; i <= keys::SCHEME_MAX; ++i) { | |
| 209 DictionaryValue* proxy_dict = NULL; | |
| 210 has_proxy[i] = proxy_rules->GetDictionary(keys::field_name[i], | |
| 211 &proxy_dict); | |
| 212 if (has_proxy[i]) { | |
| 213 net::ProxyServer::Scheme default_scheme = net::ProxyServer::SCHEME_HTTP; | |
| 214 if (!GetProxyServer(proxy_dict, default_scheme, | |
| 215 &proxy_server[i], error, bad_message)) { | |
| 216 // Don't set |error| here, as GetProxyServer takes care of that. | |
| 217 return false; | |
| 218 } | |
| 219 } | |
| 220 } | |
| 221 | |
| 222 COMPILE_ASSERT(keys::SCHEME_ALL == 0, singleProxy_must_be_first_option); | |
| 223 | |
| 224 // Handle case that only singleProxy is specified. | |
| 225 if (has_proxy[keys::SCHEME_ALL]) { | |
| 226 for (size_t i = 1; i <= keys::SCHEME_MAX; ++i) { | |
| 227 if (has_proxy[i]) { | |
| 228 *error = ExtensionErrorUtils::FormatErrorMessage( | |
| 229 "Proxy rule for * and * cannot be set at the same time.", | |
| 230 keys::field_name[keys::SCHEME_ALL], keys::field_name[i]); | |
| 231 return false; | |
| 232 } | |
| 233 } | |
| 234 *out = proxy_server[keys::SCHEME_ALL].ToURI(); | |
| 235 return true; | |
| 236 } | |
| 237 | |
| 238 // Handle case that anything but singleProxy is specified. | |
| 239 | |
| 240 // Build the proxy preference string. | |
| 241 std::string proxy_pref; | |
| 242 for (size_t i = 1; i <= keys::SCHEME_MAX; ++i) { | |
| 243 if (has_proxy[i]) { | |
| 244 // http=foopy:4010;ftp=socks5://foopy2:80 | |
| 245 if (!proxy_pref.empty()) | |
| 246 proxy_pref.append(";"); | |
| 247 proxy_pref.append(keys::scheme_name[i]); | |
| 248 proxy_pref.append("="); | |
| 249 proxy_pref.append(proxy_server[i].ToURI()); | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 *out = proxy_pref; | |
| 254 return true; | |
| 255 } | |
| 256 | |
| 257 bool JoinUrlList(ListValue* list, | |
| 258 const std::string& joiner, | |
| 259 std::string* out, | |
| 260 std::string* error, | |
| 261 bool* bad_message) { | |
| 262 std::string result; | |
| 263 for (size_t i = 0; i < list->GetSize(); ++i) { | |
| 264 if (!result.empty()) | |
| 265 result.append(joiner); | |
| 266 | |
| 267 // TODO(battre): handle UTF-8 (http://crbug.com/72692). | |
| 268 string16 entry; | |
| 269 if (!list->GetString(i, &entry)) { | |
| 270 LOG(ERROR) << "'rules.bypassList' could not be parsed."; | |
| 271 *bad_message = true; | |
| 272 return false; | |
| 273 } | |
| 274 if (!IsStringASCII(entry)) { | |
| 275 *error = "'rules.bypassList' supports only ASCII URLs " | |
| 276 "(encode URLs in Punycode format)."; | |
| 277 return false; | |
| 278 } | |
| 279 result.append(UTF16ToASCII(entry)); | |
| 280 } | |
| 281 *out = result; | |
| 282 return true; | |
| 283 } | |
| 284 | |
| 285 bool GetBypassListFromExtensionPref(const DictionaryValue* proxy_config, | |
| 286 std::string *out, | |
| 287 std::string* error, | |
| 288 bool* bad_message) { | |
| 289 DictionaryValue* proxy_rules = NULL; | |
| 290 proxy_config->GetDictionary(keys::kProxyConfigRules, &proxy_rules); | |
| 291 if (!proxy_rules) | |
| 292 return true; | |
| 293 | |
| 294 if (!proxy_rules->HasKey(keys::kProxyConfigBypassList)) { | |
| 295 *out = ""; | |
| 296 return true; | |
| 297 } | |
| 298 ListValue* bypass_list = NULL; | |
| 299 if (!proxy_rules->GetList(keys::kProxyConfigBypassList, &bypass_list)) { | |
| 300 LOG(ERROR) << "'rules.bypassList' could not be parsed."; | |
| 301 *bad_message = true; | |
| 302 return false; | |
| 303 } | |
| 304 | |
| 305 return JoinUrlList(bypass_list, ",", out, error, bad_message); | |
| 306 } | |
| 307 | |
| 308 DictionaryValue* CreateProxyConfigDict(ProxyPrefs::ProxyMode mode_enum, | |
| 309 bool pac_mandatory, | |
| 310 const std::string& pac_url, | |
| 311 const std::string& pac_data, | |
| 312 const std::string& proxy_rules_string, | |
| 313 const std::string& bypass_list, | |
| 314 std::string* error) { | |
| 315 DictionaryValue* result_proxy_config = NULL; | |
| 316 switch (mode_enum) { | |
| 317 case ProxyPrefs::MODE_DIRECT: | |
| 318 result_proxy_config = ProxyConfigDictionary::CreateDirect(); | |
| 319 break; | |
| 320 case ProxyPrefs::MODE_AUTO_DETECT: | |
| 321 result_proxy_config = ProxyConfigDictionary::CreateAutoDetect(); | |
| 322 break; | |
| 323 case ProxyPrefs::MODE_PAC_SCRIPT: { | |
| 324 std::string url; | |
| 325 if (!pac_url.empty()) { | |
| 326 url = pac_url; | |
| 327 } else if (!pac_data.empty()) { | |
| 328 if (!CreateDataURLFromPACScript(pac_data, &url)) { | |
| 329 *error = "Internal error, at base64 encoding of 'pacScript.data'."; | |
| 330 return NULL; | |
| 331 } | |
| 332 } else { | |
| 333 *error = "Proxy mode 'pac_script' requires a 'pacScript' field with " | |
| 334 "either a 'url' field or a 'data' field."; | |
| 335 return NULL; | |
| 336 } | |
| 337 result_proxy_config = | |
| 338 ProxyConfigDictionary::CreatePacScript(url, pac_mandatory); | |
| 339 break; | |
| 340 } | |
| 341 case ProxyPrefs::MODE_FIXED_SERVERS: { | |
| 342 if (proxy_rules_string.empty()) { | |
| 343 *error = "Proxy mode 'fixed_servers' requires a 'rules' field."; | |
| 344 return NULL; | |
| 345 } | |
| 346 result_proxy_config = ProxyConfigDictionary::CreateFixedServers( | |
| 347 proxy_rules_string, bypass_list); | |
| 348 break; | |
| 349 } | |
| 350 case ProxyPrefs::MODE_SYSTEM: | |
| 351 result_proxy_config = ProxyConfigDictionary::CreateSystem(); | |
| 352 break; | |
| 353 case ProxyPrefs::kModeCount: | |
| 354 NOTREACHED(); | |
| 355 } | |
| 356 return result_proxy_config; | |
| 357 } | |
| 358 | |
| 359 DictionaryValue* CreateProxyRulesDict( | |
| 360 const ProxyConfigDictionary& proxy_config) { | |
| 361 ProxyPrefs::ProxyMode mode; | |
| 362 CHECK(proxy_config.GetMode(&mode) && mode == ProxyPrefs::MODE_FIXED_SERVERS); | |
| 363 | |
| 364 scoped_ptr<DictionaryValue> extension_proxy_rules(new DictionaryValue); | |
| 365 | |
| 366 std::string proxy_servers; | |
| 367 if (!proxy_config.GetProxyServer(&proxy_servers)) { | |
| 368 LOG(ERROR) << "Missing proxy servers in configuration."; | |
| 369 return NULL; | |
| 370 } | |
| 371 | |
| 372 net::ProxyConfig::ProxyRules rules; | |
| 373 rules.ParseFromString(proxy_servers); | |
| 374 | |
| 375 switch (rules.type) { | |
| 376 case net::ProxyConfig::ProxyRules::TYPE_NO_RULES: | |
| 377 return NULL; | |
| 378 case net::ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY: | |
| 379 if (rules.single_proxy.is_valid()) { | |
| 380 extension_proxy_rules->Set(keys::field_name[keys::SCHEME_ALL], | |
| 381 CreateProxyServerDict(rules.single_proxy)); | |
| 382 } | |
| 383 break; | |
| 384 case net::ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME: | |
| 385 if (rules.proxy_for_http.is_valid()) { | |
| 386 extension_proxy_rules->Set(keys::field_name[keys::SCHEME_HTTP], | |
| 387 CreateProxyServerDict(rules.proxy_for_http)); | |
| 388 } | |
| 389 if (rules.proxy_for_https.is_valid()) { | |
| 390 extension_proxy_rules->Set( | |
| 391 keys::field_name[keys::SCHEME_HTTPS], | |
| 392 CreateProxyServerDict(rules.proxy_for_https)); | |
| 393 } | |
| 394 if (rules.proxy_for_ftp.is_valid()) { | |
| 395 extension_proxy_rules->Set(keys::field_name[keys::SCHEME_FTP], | |
| 396 CreateProxyServerDict(rules.proxy_for_ftp)); | |
| 397 } | |
| 398 if (rules.fallback_proxy.is_valid()) { | |
| 399 extension_proxy_rules->Set(keys::field_name[keys::SCHEME_FALLBACK], | |
| 400 CreateProxyServerDict(rules.fallback_proxy)); | |
| 401 } | |
| 402 break; | |
| 403 } | |
| 404 | |
| 405 // If we add a new scheme some time, we need to also store a new dictionary | |
| 406 // representing this scheme in the code above. | |
| 407 COMPILE_ASSERT(keys::SCHEME_MAX == 4, SCHEME_FORGOTTEN); | |
| 408 | |
| 409 if (proxy_config.HasBypassList()) { | |
| 410 std::string bypass_list_string; | |
| 411 if (!proxy_config.GetBypassList(&bypass_list_string)) { | |
| 412 LOG(ERROR) << "Invalid bypassList in configuration."; | |
| 413 return NULL; | |
| 414 } | |
| 415 ListValue* bypass_list = TokenizeToStringList(bypass_list_string, ",;"); | |
| 416 extension_proxy_rules->Set(keys::kProxyConfigBypassList, bypass_list); | |
| 417 } | |
| 418 | |
| 419 return extension_proxy_rules.release(); | |
| 420 } | |
| 421 | |
| 422 DictionaryValue* CreateProxyServerDict(const net::ProxyServer& proxy) { | |
| 423 scoped_ptr<DictionaryValue> out(new DictionaryValue); | |
| 424 switch (proxy.scheme()) { | |
| 425 case net::ProxyServer::SCHEME_HTTP: | |
| 426 out->SetString(keys::kProxyConfigRuleScheme, "http"); | |
| 427 break; | |
| 428 case net::ProxyServer::SCHEME_HTTPS: | |
| 429 out->SetString(keys::kProxyConfigRuleScheme, "https"); | |
| 430 break; | |
| 431 case net::ProxyServer::SCHEME_SOCKS4: | |
| 432 out->SetString(keys::kProxyConfigRuleScheme, "socks4"); | |
| 433 break; | |
| 434 case net::ProxyServer::SCHEME_SOCKS5: | |
| 435 out->SetString(keys::kProxyConfigRuleScheme, "socks5"); | |
| 436 break; | |
| 437 case net::ProxyServer::SCHEME_DIRECT: | |
| 438 case net::ProxyServer::SCHEME_INVALID: | |
| 439 NOTREACHED(); | |
| 440 return NULL; | |
| 441 } | |
| 442 out->SetString(keys::kProxyConfigRuleHost, proxy.host_port_pair().host()); | |
| 443 out->SetInteger(keys::kProxyConfigRulePort, proxy.host_port_pair().port()); | |
| 444 return out.release(); | |
| 445 } | |
| 446 | |
| 447 DictionaryValue* CreatePacScriptDict( | |
| 448 const ProxyConfigDictionary& proxy_config) { | |
| 449 ProxyPrefs::ProxyMode mode; | |
| 450 CHECK(proxy_config.GetMode(&mode) && mode == ProxyPrefs::MODE_PAC_SCRIPT); | |
| 451 | |
| 452 scoped_ptr<DictionaryValue> pac_script_dict(new DictionaryValue); | |
| 453 std::string pac_url; | |
| 454 if (!proxy_config.GetPacUrl(&pac_url)) { | |
| 455 LOG(ERROR) << "Invalid proxy configuration. Missing PAC URL."; | |
| 456 return NULL; | |
| 457 } | |
| 458 bool pac_mandatory = false; | |
| 459 if (!proxy_config.GetPacMandatory(&pac_mandatory)) { | |
| 460 LOG(ERROR) << "Invalid proxy configuration. Missing PAC mandatory field."; | |
| 461 return NULL; | |
| 462 } | |
| 463 | |
| 464 if (pac_url.find("data") == 0) { | |
| 465 std::string pac_data; | |
| 466 if (!CreatePACScriptFromDataURL(pac_url, &pac_data)) { | |
| 467 LOG(ERROR) << "Cannot decode base64-encoded PAC data URL."; | |
| 468 return NULL; | |
| 469 } | |
| 470 pac_script_dict->SetString(keys::kProxyConfigPacScriptData, pac_data); | |
| 471 } else { | |
| 472 pac_script_dict->SetString(keys::kProxyConfigPacScriptUrl, pac_url); | |
| 473 } | |
| 474 pac_script_dict->SetBoolean(keys::kProxyConfigPacScriptMandatory, | |
| 475 pac_mandatory); | |
| 476 return pac_script_dict.release(); | |
| 477 } | |
| 478 | |
| 479 ListValue* TokenizeToStringList(const std::string& in, | |
| 480 const std::string& delims) { | |
| 481 ListValue* out = new ListValue; | |
| 482 StringTokenizer entries(in, delims); | |
| 483 while (entries.GetNext()) | |
| 484 out->Append(Value::CreateStringValue(entries.token())); | |
| 485 return out; | |
| 486 } | |
| 487 | |
| 488 } // namespace extension_proxy_api_helpers | |
| OLD | NEW |