| Index: net/http/http_network_transaction.cc
|
| diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
|
| index 3269f42ade812ceb7fd6cda5044de0d16b8645b1..fe5498e7b7d2ac28f1440b147a22315bcb7307f6 100644
|
| --- a/net/http/http_network_transaction.cc
|
| +++ b/net/http/http_network_transaction.cc
|
| @@ -296,8 +296,6 @@ HttpNetworkTransaction::HttpNetworkTransaction(HttpNetworkSession* session)
|
| alternate_protocol_mode_(
|
| g_use_alternate_protocols ? kUnspecified :
|
| kDoNotUseAlternateProtocol),
|
| - embedded_identity_used_(false),
|
| - default_credentials_used_(false),
|
| read_buf_len_(0),
|
| next_state_(STATE_NONE) {
|
| session->ssl_config_service()->GetSSLConfig(&ssl_config_);
|
| @@ -397,19 +395,9 @@ int HttpNetworkTransaction::RestartWithAuth(
|
| NOTREACHED();
|
| return ERR_UNEXPECTED;
|
| }
|
| -
|
| pending_auth_target_ = HttpAuth::AUTH_NONE;
|
|
|
| - DCHECK(auth_identity_[target].invalid ||
|
| - (username.empty() && password.empty()));
|
| -
|
| - if (auth_identity_[target].invalid) {
|
| - // Update the username/password.
|
| - auth_identity_[target].source = HttpAuth::IDENT_SRC_EXTERNAL;
|
| - auth_identity_[target].invalid = false;
|
| - auth_identity_[target].username = username;
|
| - auth_identity_[target].password = password;
|
| - }
|
| + auth_controllers_[target]->ResetAuth(username, password);
|
|
|
| PrepareForAuthRestart(target);
|
|
|
| @@ -423,38 +411,6 @@ int HttpNetworkTransaction::RestartWithAuth(
|
|
|
| void HttpNetworkTransaction::PrepareForAuthRestart(HttpAuth::Target target) {
|
| DCHECK(HaveAuth(target));
|
| - DCHECK(auth_identity_[target].source != HttpAuth::IDENT_SRC_PATH_LOOKUP);
|
| -
|
| - // Add the auth entry to the cache before restarting. We don't know whether
|
| - // the identity is valid yet, but if it is valid we want other transactions
|
| - // to know about it. If an entry for (origin, handler->realm()) already
|
| - // exists, we update it.
|
| - //
|
| - // If auth_identity_[target].source is HttpAuth::IDENT_SRC_NONE or
|
| - // HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS, auth_identity_[target] contains
|
| - // no identity because identity is not required yet or we're using default
|
| - // credentials.
|
| - //
|
| - // TODO(wtc): For NTLM_SSPI, we add the same auth entry to the cache in
|
| - // round 1 and round 2, which is redundant but correct. It would be nice
|
| - // to add an auth entry to the cache only once, preferrably in round 1.
|
| - // See http://crbug.com/21015.
|
| - switch (auth_identity_[target].source) {
|
| - case HttpAuth::IDENT_SRC_NONE:
|
| - case HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS:
|
| - break;
|
| - default:
|
| - session_->auth_cache()->Add(
|
| - AuthOrigin(target),
|
| - auth_handler_[target]->realm(),
|
| - auth_handler_[target]->scheme(),
|
| - auth_handler_[target]->challenge(),
|
| - auth_identity_[target].username,
|
| - auth_identity_[target].password,
|
| - AuthPath(target));
|
| - break;
|
| - }
|
| -
|
| bool keep_alive = false;
|
| // Even if the server says the connection is keep-alive, we have to be
|
| // able to find the end of each response in order to reuse the connection.
|
| @@ -856,6 +812,16 @@ int HttpNetworkTransaction::DoInitConnection() {
|
| DCHECK(!connection_->is_initialized());
|
| DCHECK(proxy_info_.proxy_server().is_valid());
|
|
|
| + // Now that the proxy server has been resolved, create the auth_controllers_.
|
| + for (int i = 0; i < HttpAuth::AUTH_NUM_TARGETS; i++) {
|
| + HttpAuth::Target target = static_cast<HttpAuth::Target>(i);
|
| + if (!auth_controllers_[target].get())
|
| + auth_controllers_[target].reset(new HttpAuthController(target,
|
| + AuthURL(target),
|
| + session_,
|
| + net_log_));
|
| + }
|
| +
|
| next_state_ = STATE_INIT_CONNECTION_COMPLETE;
|
|
|
| using_ssl_ = request_->url.SchemeIs("https") ||
|
| @@ -1003,7 +969,8 @@ void HttpNetworkTransaction::ClearTunnelState() {
|
|
|
| int HttpNetworkTransaction::DoTunnelGenerateAuthToken() {
|
| next_state_ = STATE_TUNNEL_GENERATE_AUTH_TOKEN_COMPLETE;
|
| - return MaybeGenerateAuthToken(HttpAuth::AUTH_PROXY);
|
| + return auth_controllers_[HttpAuth::AUTH_PROXY]->MaybeGenerateAuthToken(
|
| + request_, &io_callback_);
|
| }
|
|
|
| int HttpNetworkTransaction::DoTunnelGenerateAuthTokenComplete(int rv) {
|
| @@ -1021,7 +988,8 @@ int HttpNetworkTransaction::DoTunnelSendRequest() {
|
| if (request_headers_.empty()) {
|
| HttpRequestHeaders authorization_headers;
|
| if (HaveAuth(HttpAuth::AUTH_PROXY))
|
| - AddAuthorizationHeader(HttpAuth::AUTH_PROXY, &authorization_headers);
|
| + auth_controllers_[HttpAuth::AUTH_PROXY]->AddAuthorizationHeader(
|
| + &authorization_headers);
|
| std::string request_line;
|
| HttpRequestHeaders request_headers;
|
| BuildTunnelRequest(request_, authorization_headers, endpoint_,
|
| @@ -1213,7 +1181,8 @@ int HttpNetworkTransaction::DoGenerateProxyAuthToken() {
|
| next_state_ = STATE_GENERATE_PROXY_AUTH_TOKEN_COMPLETE;
|
| if (!ShouldApplyProxyAuth())
|
| return OK;
|
| - return MaybeGenerateAuthToken(HttpAuth::AUTH_PROXY);
|
| + return auth_controllers_[HttpAuth::AUTH_PROXY]->MaybeGenerateAuthToken(
|
| + request_, &io_callback_);
|
| }
|
|
|
| int HttpNetworkTransaction::DoGenerateProxyAuthTokenComplete(int rv) {
|
| @@ -1227,7 +1196,8 @@ int HttpNetworkTransaction::DoGenerateServerAuthToken() {
|
| next_state_ = STATE_GENERATE_SERVER_AUTH_TOKEN_COMPLETE;
|
| if (!ShouldApplyServerAuth())
|
| return OK;
|
| - return MaybeGenerateAuthToken(HttpAuth::AUTH_SERVER);
|
| + return auth_controllers_[HttpAuth::AUTH_SERVER]->MaybeGenerateAuthToken(
|
| + request_, &io_callback_);
|
| }
|
|
|
| int HttpNetworkTransaction::DoGenerateServerAuthTokenComplete(int rv) {
|
| @@ -1259,9 +1229,11 @@ int HttpNetworkTransaction::DoSendRequest() {
|
| bool have_server_auth = (ShouldApplyServerAuth() &&
|
| HaveAuth(HttpAuth::AUTH_SERVER));
|
| if (have_proxy_auth)
|
| - AddAuthorizationHeader(HttpAuth::AUTH_PROXY, &authorization_headers);
|
| + auth_controllers_[HttpAuth::AUTH_PROXY]->AddAuthorizationHeader(
|
| + &authorization_headers);
|
| if (have_server_auth)
|
| - AddAuthorizationHeader(HttpAuth::AUTH_SERVER, &authorization_headers);
|
| + auth_controllers_[HttpAuth::AUTH_SERVER]->AddAuthorizationHeader(
|
| + &authorization_headers);
|
| std::string request_line;
|
| HttpRequestHeaders request_headers;
|
| BuildRequestHeaders(request_, authorization_headers, request_body,
|
| @@ -1409,10 +1381,10 @@ int HttpNetworkTransaction::DoReadHeadersComplete(int result) {
|
| }
|
|
|
| int HttpNetworkTransaction::DoResolveCanonicalName() {
|
| - DCHECK(auth_handler_[pending_auth_target_].get());
|
| + DCHECK(auth_controllers_[pending_auth_target_].get());
|
| next_state_ = STATE_RESOLVE_CANONICAL_NAME_COMPLETE;
|
| - return auth_handler_[pending_auth_target_]->
|
| - ResolveCanonicalName(session_->host_resolver(), &io_callback_);
|
| + return auth_controllers_[pending_auth_target_]->ResolveCanonicalName(
|
| + &io_callback_);
|
| }
|
|
|
| int HttpNetworkTransaction::DoResolveCanonicalNameComplete(int result) {
|
| @@ -1971,203 +1943,6 @@ bool HttpNetworkTransaction::ShouldApplyServerAuth() const {
|
| return !(request_->load_flags & LOAD_DO_NOT_SEND_AUTH_DATA);
|
| }
|
|
|
| -int HttpNetworkTransaction::MaybeGenerateAuthToken(HttpAuth::Target target) {
|
| - bool needs_auth = HaveAuth(target) || SelectPreemptiveAuth(target);
|
| - if (!needs_auth)
|
| - return OK;
|
| - const std::wstring* username = NULL;
|
| - const std::wstring* password = NULL;
|
| - const HttpAuth::Identity& identity = auth_identity_[target];
|
| - if (identity.source != HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS) {
|
| - username = &identity.username;
|
| - password = &identity.password;
|
| - }
|
| - DCHECK(auth_token_[target].empty());
|
| - return auth_handler_[target]->GenerateAuthToken(
|
| - username, password, request_, &io_callback_, &auth_token_[target]);
|
| -}
|
| -
|
| -void HttpNetworkTransaction::AddAuthorizationHeader(
|
| - HttpAuth::Target target, HttpRequestHeaders* authorization_headers) {
|
| - DCHECK(HaveAuth(target));
|
| - DCHECK(!auth_token_[target].empty());
|
| - authorization_headers->SetHeader(
|
| - HttpAuth::GetAuthorizationHeaderName(target),
|
| - auth_token_[target]);
|
| - auth_token_[target].clear();
|
| -}
|
| -
|
| -GURL HttpNetworkTransaction::AuthOrigin(HttpAuth::Target target) const {
|
| - GURL origin = PossiblyInvalidAuthOrigin(target);
|
| - DCHECK(origin.is_valid());
|
| - return origin;
|
| -}
|
| -
|
| -GURL HttpNetworkTransaction::PossiblyInvalidAuthOrigin(
|
| - HttpAuth::Target target) const {
|
| - switch (target) {
|
| - case HttpAuth::AUTH_PROXY:
|
| - if (!proxy_info_.proxy_server().is_valid() ||
|
| - proxy_info_.proxy_server().is_direct()) {
|
| - return GURL(); // There is no proxy server.
|
| - }
|
| - return GURL("http://" + proxy_info_.proxy_server().host_and_port());
|
| - case HttpAuth::AUTH_SERVER:
|
| - return request_->url.GetOrigin();
|
| - default:
|
| - return GURL();
|
| - }
|
| -}
|
| -
|
| -std::string HttpNetworkTransaction::AuthPath(HttpAuth::Target target)
|
| - const {
|
| - // Proxy authentication realms apply to all paths. So we will use
|
| - // empty string in place of an absolute path.
|
| - return target == HttpAuth::AUTH_PROXY ?
|
| - std::string() : request_->url.path();
|
| -}
|
| -
|
| -// static
|
| -std::string HttpNetworkTransaction::AuthTargetString(
|
| - HttpAuth::Target target) {
|
| - return target == HttpAuth::AUTH_PROXY ? "proxy" : "server";
|
| -}
|
| -
|
| -void HttpNetworkTransaction::InvalidateRejectedAuthFromCache(
|
| - HttpAuth::Target target,
|
| - const GURL& auth_origin) {
|
| - DCHECK(HaveAuth(target));
|
| -
|
| - // TODO(eroman): this short-circuit can be relaxed. If the realm of
|
| - // the preemptively used auth entry matches the realm of the subsequent
|
| - // challenge, then we can invalidate the preemptively used entry.
|
| - // Otherwise as-is we may send the failed credentials one extra time.
|
| - if (auth_identity_[target].source == HttpAuth::IDENT_SRC_PATH_LOOKUP)
|
| - return;
|
| -
|
| - // Clear the cache entry for the identity we just failed on.
|
| - // Note: we require the username/password to match before invalidating
|
| - // since the entry in the cache may be newer than what we used last time.
|
| - session_->auth_cache()->Remove(auth_origin,
|
| - auth_handler_[target]->realm(),
|
| - auth_handler_[target]->scheme(),
|
| - auth_identity_[target].username,
|
| - auth_identity_[target].password);
|
| -}
|
| -
|
| -bool HttpNetworkTransaction::SelectPreemptiveAuth(HttpAuth::Target target) {
|
| - DCHECK(!HaveAuth(target));
|
| -
|
| - // Don't do preemptive authorization if the URL contains a username/password,
|
| - // since we must first be challenged in order to use the URL's identity.
|
| - if (request_->url.has_username())
|
| - return false;
|
| -
|
| - // SelectPreemptiveAuth() is on the critical path for each request, so it
|
| - // is expected to be fast. LookupByPath() is fast in the common case, since
|
| - // the number of http auth cache entries is expected to be very small.
|
| - // (For most users in fact, it will be 0.)
|
| - HttpAuthCache::Entry* entry = session_->auth_cache()->LookupByPath(
|
| - AuthOrigin(target), AuthPath(target));
|
| - if (!entry)
|
| - return false;
|
| -
|
| - // Try to create a handler using the previous auth challenge.
|
| - scoped_ptr<HttpAuthHandler> handler_preemptive;
|
| - int rv_create = session_->http_auth_handler_factory()->
|
| - CreatePreemptiveAuthHandlerFromString(
|
| - entry->auth_challenge(), target, AuthOrigin(target),
|
| - entry->IncrementNonceCount(), net_log_, &handler_preemptive);
|
| - if (rv_create != OK)
|
| - return false;
|
| -
|
| - // Set the state
|
| - auth_identity_[target].source = HttpAuth::IDENT_SRC_PATH_LOOKUP;
|
| - auth_identity_[target].invalid = false;
|
| - auth_identity_[target].username = entry->username();
|
| - auth_identity_[target].password = entry->password();
|
| - auth_handler_[target].swap(handler_preemptive);
|
| - return true;
|
| -}
|
| -
|
| -bool HttpNetworkTransaction::SelectNextAuthIdentityToTry(
|
| - HttpAuth::Target target,
|
| - const GURL& auth_origin) {
|
| - DCHECK(auth_handler_[target].get());
|
| - DCHECK(auth_identity_[target].invalid);
|
| -
|
| - // Try to use the username/password encoded into the URL first.
|
| - if (target == HttpAuth::AUTH_SERVER && request_->url.has_username() &&
|
| - !embedded_identity_used_) {
|
| - auth_identity_[target].source = HttpAuth::IDENT_SRC_URL;
|
| - auth_identity_[target].invalid = false;
|
| - // Extract the username:password from the URL.
|
| - GetIdentityFromURL(request_->url,
|
| - &auth_identity_[target].username,
|
| - &auth_identity_[target].password);
|
| - embedded_identity_used_ = true;
|
| - // TODO(eroman): If the password is blank, should we also try combining
|
| - // with a password from the cache?
|
| - return true;
|
| - }
|
| -
|
| - // Check the auth cache for a realm entry.
|
| - HttpAuthCache::Entry* entry =
|
| - session_->auth_cache()->Lookup(auth_origin, auth_handler_[target]->realm(),
|
| - auth_handler_[target]->scheme());
|
| -
|
| - if (entry) {
|
| - auth_identity_[target].source = HttpAuth::IDENT_SRC_REALM_LOOKUP;
|
| - auth_identity_[target].invalid = false;
|
| - auth_identity_[target].username = entry->username();
|
| - auth_identity_[target].password = entry->password();
|
| - return true;
|
| - }
|
| -
|
| - // Use default credentials (single sign on) if this is the first attempt
|
| - // at identity. Do not allow multiple times as it will infinite loop.
|
| - // We use default credentials after checking the auth cache so that if
|
| - // single sign-on doesn't work, we won't try default credentials for future
|
| - // transactions.
|
| - if (!default_credentials_used_ &&
|
| - auth_handler_[target]->AllowsDefaultCredentials()) {
|
| - auth_identity_[target].source = HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS;
|
| - auth_identity_[target].invalid = false;
|
| - default_credentials_used_ = true;
|
| - return true;
|
| - }
|
| -
|
| - return false;
|
| -}
|
| -
|
| -std::string HttpNetworkTransaction::AuthChallengeLogMessage() const {
|
| - std::string msg;
|
| - std::string header_val;
|
| - void* iter = NULL;
|
| - scoped_refptr<HttpResponseHeaders> headers = GetResponseHeaders();
|
| - while (headers->EnumerateHeader(&iter, "proxy-authenticate", &header_val)) {
|
| - msg.append("\n Has header Proxy-Authenticate: ");
|
| - msg.append(header_val);
|
| - }
|
| -
|
| - iter = NULL;
|
| - while (headers->EnumerateHeader(&iter, "www-authenticate", &header_val)) {
|
| - msg.append("\n Has header WWW-Authenticate: ");
|
| - msg.append(header_val);
|
| - }
|
| -
|
| - // RFC 4559 requires that a proxy indicate its support of NTLM/Negotiate
|
| - // authentication with a "Proxy-Support: Session-Based-Authentication"
|
| - // response header.
|
| - iter = NULL;
|
| - while (headers->EnumerateHeader(&iter, "proxy-support", &header_val)) {
|
| - msg.append("\n Has header Proxy-Support: ");
|
| - msg.append(header_val);
|
| - }
|
| -
|
| - return msg;
|
| -}
|
| -
|
| int HttpNetworkTransaction::HandleAuthChallenge(bool establishing_tunnel) {
|
| scoped_refptr<HttpResponseHeaders> headers = GetResponseHeaders();
|
| DCHECK(headers);
|
| @@ -2177,99 +1952,41 @@ int HttpNetworkTransaction::HandleAuthChallenge(bool establishing_tunnel) {
|
| return OK;
|
| HttpAuth::Target target = status == 407 ?
|
| HttpAuth::AUTH_PROXY : HttpAuth::AUTH_SERVER;
|
| - GURL auth_origin = PossiblyInvalidAuthOrigin(target);
|
| -
|
| - LOG(INFO) << "The " << AuthTargetString(target) << " "
|
| - << auth_origin << " requested auth"
|
| - << AuthChallengeLogMessage();
|
| -
|
| if (target == HttpAuth::AUTH_PROXY && proxy_info_.is_direct())
|
| return ERR_UNEXPECTED_PROXY_AUTH;
|
| - DCHECK(auth_origin.is_valid());
|
| -
|
| - // The auth we tried just failed, hence it can't be valid. Remove it from
|
| - // the cache so it won't be used again.
|
| - // TODO(wtc): IsFinalRound is not the right condition. In a multi-round
|
| - // auth sequence, the server may fail the auth in round 1 if our first
|
| - // authorization header is broken. We should inspect response_.headers to
|
| - // determine if the server already failed the auth or wants us to continue.
|
| - // See http://crbug.com/21015.
|
| - if (HaveAuth(target) && auth_handler_[target]->IsFinalRound()) {
|
| - InvalidateRejectedAuthFromCache(target, auth_origin);
|
| - auth_handler_[target].reset();
|
| - auth_identity_[target] = HttpAuth::Identity();
|
| - }
|
| -
|
| - auth_identity_[target].invalid = true;
|
| -
|
| - if (target != HttpAuth::AUTH_SERVER ||
|
| - !(request_->load_flags & LOAD_DO_NOT_SEND_AUTH_DATA)) {
|
| - // Find the best authentication challenge that we support.
|
| - HttpAuth::ChooseBestChallenge(session_->http_auth_handler_factory(),
|
| - headers, target, auth_origin, net_log_,
|
| - &auth_handler_[target]);
|
| - }
|
| -
|
| - if (!auth_handler_[target].get()) {
|
| - if (establishing_tunnel) {
|
| - LOG(ERROR) << "Can't perform auth to the " << AuthTargetString(target)
|
| - << " " << auth_origin << " when establishing a tunnel"
|
| - << AuthChallengeLogMessage();
|
| -
|
| - // We are establishing a tunnel, we can't show the error page because an
|
| - // active network attacker could control its contents. Instead, we just
|
| - // fail to establish the tunnel.
|
| - DCHECK(target == HttpAuth::AUTH_PROXY);
|
| - return ERR_PROXY_AUTH_REQUESTED;
|
| - }
|
| - // We found no supported challenge -- let the transaction continue
|
| - // so we end up displaying the error page.
|
| - return OK;
|
| - }
|
|
|
| - if (auth_handler_[target]->NeedsIdentity()) {
|
| - // Pick a new auth identity to try, by looking to the URL and auth cache.
|
| - // If an identity to try is found, it is saved to auth_identity_[target].
|
| - SelectNextAuthIdentityToTry(target, auth_origin);
|
| - } else {
|
| - // Proceed with the existing identity or a null identity.
|
| - //
|
| - // TODO(wtc): Add a safeguard against infinite transaction restarts, if
|
| - // the server keeps returning "NTLM".
|
| - auth_identity_[target].invalid = false;
|
| - }
|
| + int rv = auth_controllers_[target]->HandleAuthChallenge(headers,
|
| + request_->load_flags,
|
| + establishing_tunnel);
|
| + if (auth_controllers_[target]->HaveAuthHandler())
|
| + pending_auth_target_ = target;
|
|
|
| - // Make a note that we are waiting for auth. This variable is inspected
|
| - // when the client calls RestartWithAuth() to pick up where we left off.
|
| - pending_auth_target_ = target;
|
| + scoped_refptr<AuthChallengeInfo> auth_info =
|
| + auth_controllers_[target]->auth_info();
|
| + if (auth_info.get())
|
| + response_.auth_challenge = auth_info;
|
|
|
| - if (auth_identity_[target].invalid) {
|
| - // We have exhausted all identity possibilities, all we can do now is
|
| - // pass the challenge information back to the client.
|
| - PopulateAuthChallenge(target, auth_origin);
|
| + if (rv == ERR_AUTH_NEEDS_CANONICAL_NAME) {
|
| + next_state_ = STATE_RESOLVE_CANONICAL_NAME;
|
| + rv = OK;
|
| }
|
|
|
| - // SPN determination (for Negotiate) requires a DNS lookup to find the
|
| - // canonical name. This needs to be done asynchronously to prevent blocking
|
| - // the IO thread.
|
| - if (auth_handler_[target]->NeedsCanonicalName())
|
| - next_state_ = STATE_RESOLVE_CANONICAL_NAME;
|
| -
|
| - return OK;
|
| + return rv;
|
| }
|
|
|
| -void HttpNetworkTransaction::PopulateAuthChallenge(HttpAuth::Target target,
|
| - const GURL& auth_origin) {
|
| - // Populates response_.auth_challenge with the authentication challenge info.
|
| - // This info is consumed by URLRequestHttpJob::GetAuthChallengeInfo().
|
| -
|
| - AuthChallengeInfo* auth_info = new AuthChallengeInfo;
|
| - auth_info->is_proxy = target == HttpAuth::AUTH_PROXY;
|
| - auth_info->host_and_port = ASCIIToWide(GetHostAndPort(auth_origin));
|
| - auth_info->scheme = ASCIIToWide(auth_handler_[target]->scheme());
|
| - // TODO(eroman): decode realm according to RFC 2047.
|
| - auth_info->realm = ASCIIToWide(auth_handler_[target]->realm());
|
| - response_.auth_challenge = auth_info;
|
| +GURL HttpNetworkTransaction::AuthURL(HttpAuth::Target target) const {
|
| + switch (target) {
|
| + case HttpAuth::AUTH_PROXY:
|
| + if (!proxy_info_.proxy_server().is_valid() ||
|
| + proxy_info_.proxy_server().is_direct()) {
|
| + return GURL(); // There is no proxy server.
|
| + }
|
| + return GURL("http://" + proxy_info_.proxy_server().host_and_port());
|
| + case HttpAuth::AUTH_SERVER:
|
| + return request_->url;
|
| + default:
|
| + return GURL();
|
| + }
|
| }
|
|
|
| void HttpNetworkTransaction::MarkBrokenAlternateProtocolAndFallback() {
|
|
|