Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // HttpLog.h should generally be included first
8 #include "HttpLog.h"
10 #include "nsHttp.h"
11 #include "nsHttpDigestAuth.h"
12 #include "nsIHttpAuthenticableChannel.h"
13 #include "nsISupportsPrimitives.h"
14 #include "nsIURI.h"
15 #include "nsString.h"
16 #include "nsEscape.h"
17 #include "nsNetCID.h"
18 #include "prprf.h"
19 #include "nsCRT.h"
20 #include "nsICryptoHash.h"
22 namespace mozilla {
23 namespace net {
25 //-----------------------------------------------------------------------------
26 // nsHttpDigestAuth <public>
27 //-----------------------------------------------------------------------------
29 nsHttpDigestAuth::nsHttpDigestAuth()
30 {}
32 nsHttpDigestAuth::~nsHttpDigestAuth()
33 {}
35 //-----------------------------------------------------------------------------
36 // nsHttpDigestAuth::nsISupports
37 //-----------------------------------------------------------------------------
39 NS_IMPL_ISUPPORTS(nsHttpDigestAuth, nsIHttpAuthenticator)
41 //-----------------------------------------------------------------------------
42 // nsHttpDigestAuth <protected>
43 //-----------------------------------------------------------------------------
45 nsresult
46 nsHttpDigestAuth::MD5Hash(const char *buf, uint32_t len)
47 {
48 nsresult rv;
50 // Cache a reference to the nsICryptoHash instance since we'll be calling
51 // this function frequently.
52 if (!mVerifier) {
53 mVerifier = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
54 if (NS_FAILED(rv)) {
55 LOG(("nsHttpDigestAuth: no crypto hash!\n"));
56 return rv;
57 }
58 }
60 rv = mVerifier->Init(nsICryptoHash::MD5);
61 if (NS_FAILED(rv)) return rv;
63 rv = mVerifier->Update((unsigned char*)buf, len);
64 if (NS_FAILED(rv)) return rv;
66 nsAutoCString hashString;
67 rv = mVerifier->Finish(false, hashString);
68 if (NS_FAILED(rv)) return rv;
70 NS_ENSURE_STATE(hashString.Length() == sizeof(mHashBuf));
71 memcpy(mHashBuf, hashString.get(), hashString.Length());
73 return rv;
74 }
76 nsresult
77 nsHttpDigestAuth::GetMethodAndPath(nsIHttpAuthenticableChannel *authChannel,
78 bool isProxyAuth,
79 nsCString &httpMethod,
80 nsCString &path)
81 {
82 nsresult rv, rv2;
83 nsCOMPtr<nsIURI> uri;
84 rv = authChannel->GetURI(getter_AddRefs(uri));
85 if (NS_SUCCEEDED(rv)) {
86 bool proxyMethodIsConnect;
87 rv = authChannel->GetProxyMethodIsConnect(&proxyMethodIsConnect);
88 if (NS_SUCCEEDED(rv)) {
89 if (proxyMethodIsConnect && isProxyAuth) {
90 httpMethod.AssignLiteral("CONNECT");
91 //
92 // generate hostname:port string. (unfortunately uri->GetHostPort
93 // leaves out the port if it matches the default value, so we can't
94 // just call it.)
95 //
96 int32_t port;
97 rv = uri->GetAsciiHost(path);
98 rv2 = uri->GetPort(&port);
99 if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2)) {
100 path.Append(':');
101 path.AppendInt(port < 0 ? NS_HTTPS_DEFAULT_PORT : port);
102 }
103 }
104 else {
105 rv = authChannel->GetRequestMethod(httpMethod);
106 rv2 = uri->GetPath(path);
107 if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2)) {
108 //
109 // strip any fragment identifier from the URL path.
110 //
111 int32_t ref = path.RFindChar('#');
112 if (ref != kNotFound)
113 path.Truncate(ref);
114 //
115 // make sure we escape any UTF-8 characters in the URI path. the
116 // digest auth uri attribute needs to match the request-URI.
117 //
118 // XXX we should really ask the HTTP channel for this string
119 // instead of regenerating it here.
120 //
121 nsAutoCString buf;
122 path = NS_EscapeURL(path, esc_OnlyNonASCII, buf);
123 }
124 }
125 }
126 }
127 return rv;
128 }
130 //-----------------------------------------------------------------------------
131 // nsHttpDigestAuth::nsIHttpAuthenticator
132 //-----------------------------------------------------------------------------
134 NS_IMETHODIMP
135 nsHttpDigestAuth::ChallengeReceived(nsIHttpAuthenticableChannel *authChannel,
136 const char *challenge,
137 bool isProxyAuth,
138 nsISupports **sessionState,
139 nsISupports **continuationState,
140 bool *result)
141 {
142 nsAutoCString realm, domain, nonce, opaque;
143 bool stale;
144 uint16_t algorithm, qop;
146 nsresult rv = ParseChallenge(challenge, realm, domain, nonce, opaque,
147 &stale, &algorithm, &qop);
148 if (NS_FAILED(rv)) return rv;
150 // if the challenge has the "stale" flag set, then the user identity is not
151 // necessarily invalid. by returning FALSE here we can suppress username
152 // and password prompting that usually accompanies a 401/407 challenge.
153 *result = !stale;
155 // clear any existing nonce_count since we have a new challenge.
156 NS_IF_RELEASE(*sessionState);
157 return NS_OK;
158 }
160 NS_IMETHODIMP
161 nsHttpDigestAuth::GenerateCredentials(nsIHttpAuthenticableChannel *authChannel,
162 const char *challenge,
163 bool isProxyAuth,
164 const char16_t *userdomain,
165 const char16_t *username,
166 const char16_t *password,
167 nsISupports **sessionState,
168 nsISupports **continuationState,
169 uint32_t *aFlags,
170 char **creds)
172 {
173 LOG(("nsHttpDigestAuth::GenerateCredentials [challenge=%s]\n", challenge));
175 NS_ENSURE_ARG_POINTER(creds);
177 *aFlags = 0;
179 bool isDigestAuth = !PL_strncasecmp(challenge, "digest ", 7);
180 NS_ENSURE_TRUE(isDigestAuth, NS_ERROR_UNEXPECTED);
182 // IIS implementation requires extra quotes
183 bool requireExtraQuotes = false;
184 {
185 nsAutoCString serverVal;
186 authChannel->GetServerResponseHeader(serverVal);
187 if (!serverVal.IsEmpty()) {
188 requireExtraQuotes = !PL_strncasecmp(serverVal.get(), "Microsoft-IIS", 13);
189 }
190 }
192 nsresult rv;
193 nsAutoCString httpMethod;
194 nsAutoCString path;
195 rv = GetMethodAndPath(authChannel, isProxyAuth, httpMethod, path);
196 if (NS_FAILED(rv)) return rv;
198 nsAutoCString realm, domain, nonce, opaque;
199 bool stale;
200 uint16_t algorithm, qop;
202 rv = ParseChallenge(challenge, realm, domain, nonce, opaque,
203 &stale, &algorithm, &qop);
204 if (NS_FAILED(rv)) {
205 LOG(("nsHttpDigestAuth::GenerateCredentials [ParseChallenge failed rv=%x]\n", rv));
206 return rv;
207 }
209 char ha1_digest[EXPANDED_DIGEST_LENGTH+1];
210 char ha2_digest[EXPANDED_DIGEST_LENGTH+1];
211 char response_digest[EXPANDED_DIGEST_LENGTH+1];
212 char upload_data_digest[EXPANDED_DIGEST_LENGTH+1];
214 if (qop & QOP_AUTH_INT) {
215 // we do not support auth-int "quality of protection" currently
216 qop &= ~QOP_AUTH_INT;
218 NS_WARNING("no support for Digest authentication with data integrity quality of protection");
220 /* TODO: to support auth-int, we need to get an MD5 digest of
221 * TODO: the data uploaded with this request.
222 * TODO: however, i am not sure how to read in the file in without
223 * TODO: disturbing the channel''s use of it. do i need to copy it
224 * TODO: somehow?
225 */
226 #if 0
227 if (http_channel != nullptr)
228 {
229 nsIInputStream * upload;
230 nsCOMPtr<nsIUploadChannel> uc = do_QueryInterface(http_channel);
231 NS_ENSURE_TRUE(uc, NS_ERROR_UNEXPECTED);
232 uc->GetUploadStream(&upload);
233 if (upload) {
234 char * upload_buffer;
235 int upload_buffer_length = 0;
236 //TODO: read input stream into buffer
237 const char * digest = (const char*)
238 nsNetwerkMD5Digest(upload_buffer, upload_buffer_length);
239 ExpandToHex(digest, upload_data_digest);
240 NS_RELEASE(upload);
241 }
242 }
243 #endif
244 }
246 if (!(algorithm & ALGO_MD5 || algorithm & ALGO_MD5_SESS)) {
247 // they asked only for algorithms that we do not support
248 NS_WARNING("unsupported algorithm requested by Digest authentication");
249 return NS_ERROR_NOT_IMPLEMENTED;
250 }
252 //
253 // the following are for increasing security. see RFC 2617 for more
254 // information.
255 //
256 // nonce_count allows the server to keep track of auth challenges (to help
257 // prevent spoofing). we increase this count every time.
258 //
259 char nonce_count[NONCE_COUNT_LENGTH+1] = "00000001"; // in hex
260 if (*sessionState) {
261 nsCOMPtr<nsISupportsPRUint32> v(do_QueryInterface(*sessionState));
262 if (v) {
263 uint32_t nc;
264 v->GetData(&nc);
265 PR_snprintf(nonce_count, sizeof(nonce_count), "%08x", ++nc);
266 v->SetData(nc);
267 }
268 }
269 else {
270 nsCOMPtr<nsISupportsPRUint32> v(
271 do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID));
272 if (v) {
273 v->SetData(1);
274 NS_ADDREF(*sessionState = v);
275 }
276 }
277 LOG((" nonce_count=%s\n", nonce_count));
279 //
280 // this lets the client verify the server response (via a server
281 // returned Authentication-Info header). also used for session info.
282 //
283 nsAutoCString cnonce;
284 static const char hexChar[] = "0123456789abcdef";
285 for (int i=0; i<16; ++i) {
286 cnonce.Append(hexChar[(int)(15.0 * rand()/(RAND_MAX + 1.0))]);
287 }
288 LOG((" cnonce=%s\n", cnonce.get()));
290 //
291 // calculate credentials
292 //
294 NS_ConvertUTF16toUTF8 cUser(username), cPass(password);
295 rv = CalculateHA1(cUser, cPass, realm, algorithm, nonce, cnonce, ha1_digest);
296 if (NS_FAILED(rv)) return rv;
298 rv = CalculateHA2(httpMethod, path, qop, upload_data_digest, ha2_digest);
299 if (NS_FAILED(rv)) return rv;
301 rv = CalculateResponse(ha1_digest, ha2_digest, nonce, qop, nonce_count,
302 cnonce, response_digest);
303 if (NS_FAILED(rv)) return rv;
305 //
306 // Values that need to match the quoted-string production from RFC 2616:
307 //
308 // username
309 // realm
310 // nonce
311 // opaque
312 // cnonce
313 //
315 nsAutoCString authString;
317 authString.AssignLiteral("Digest username=");
318 rv = AppendQuotedString(cUser, authString);
319 NS_ENSURE_SUCCESS(rv, rv);
321 authString.AppendLiteral(", realm=");
322 rv = AppendQuotedString(realm, authString);
323 NS_ENSURE_SUCCESS(rv, rv);
325 authString.AppendLiteral(", nonce=");
326 rv = AppendQuotedString(nonce, authString);
327 NS_ENSURE_SUCCESS(rv, rv);
329 authString.AppendLiteral(", uri=\"");
330 authString += path;
331 if (algorithm & ALGO_SPECIFIED) {
332 authString.AppendLiteral("\", algorithm=");
333 if (algorithm & ALGO_MD5_SESS)
334 authString.AppendLiteral("MD5-sess");
335 else
336 authString.AppendLiteral("MD5");
337 } else {
338 authString += '\"';
339 }
340 authString.AppendLiteral(", response=\"");
341 authString += response_digest;
342 authString += '\"';
344 if (!opaque.IsEmpty()) {
345 authString.AppendLiteral(", opaque=");
346 rv = AppendQuotedString(opaque, authString);
347 NS_ENSURE_SUCCESS(rv, rv);
348 }
350 if (qop) {
351 authString.AppendLiteral(", qop=");
352 if (requireExtraQuotes)
353 authString += '\"';
354 authString.AppendLiteral("auth");
355 if (qop & QOP_AUTH_INT)
356 authString.AppendLiteral("-int");
357 if (requireExtraQuotes)
358 authString += '\"';
359 authString.AppendLiteral(", nc=");
360 authString += nonce_count;
362 authString.AppendLiteral(", cnonce=");
363 rv = AppendQuotedString(cnonce, authString);
364 NS_ENSURE_SUCCESS(rv, rv);
365 }
368 *creds = ToNewCString(authString);
369 return NS_OK;
370 }
372 NS_IMETHODIMP
373 nsHttpDigestAuth::GetAuthFlags(uint32_t *flags)
374 {
375 *flags = REQUEST_BASED | REUSABLE_CHALLENGE | IDENTITY_ENCRYPTED;
376 //
377 // NOTE: digest auth credentials must be uniquely computed for each request,
378 // so we do not set the REUSABLE_CREDENTIALS flag.
379 //
380 return NS_OK;
381 }
383 nsresult
384 nsHttpDigestAuth::CalculateResponse(const char * ha1_digest,
385 const char * ha2_digest,
386 const nsAFlatCString & nonce,
387 uint16_t qop,
388 const char * nonce_count,
389 const nsAFlatCString & cnonce,
390 char * result)
391 {
392 uint32_t len = 2*EXPANDED_DIGEST_LENGTH + nonce.Length() + 2;
394 if (qop & QOP_AUTH || qop & QOP_AUTH_INT) {
395 len += cnonce.Length() + NONCE_COUNT_LENGTH + 3;
396 if (qop & QOP_AUTH_INT)
397 len += 8; // length of "auth-int"
398 else
399 len += 4; // length of "auth"
400 }
402 nsAutoCString contents;
403 contents.SetCapacity(len);
405 contents.Assign(ha1_digest, EXPANDED_DIGEST_LENGTH);
406 contents.Append(':');
407 contents.Append(nonce);
408 contents.Append(':');
410 if (qop & QOP_AUTH || qop & QOP_AUTH_INT) {
411 contents.Append(nonce_count, NONCE_COUNT_LENGTH);
412 contents.Append(':');
413 contents.Append(cnonce);
414 contents.Append(':');
415 if (qop & QOP_AUTH_INT)
416 contents.AppendLiteral("auth-int:");
417 else
418 contents.AppendLiteral("auth:");
419 }
421 contents.Append(ha2_digest, EXPANDED_DIGEST_LENGTH);
423 nsresult rv = MD5Hash(contents.get(), contents.Length());
424 if (NS_SUCCEEDED(rv))
425 rv = ExpandToHex(mHashBuf, result);
426 return rv;
427 }
429 nsresult
430 nsHttpDigestAuth::ExpandToHex(const char * digest, char * result)
431 {
432 int16_t index, value;
434 for (index = 0; index < DIGEST_LENGTH; index++) {
435 value = (digest[index] >> 4) & 0xf;
436 if (value < 10)
437 result[index*2] = value + '0';
438 else
439 result[index*2] = value - 10 + 'a';
441 value = digest[index] & 0xf;
442 if (value < 10)
443 result[(index*2)+1] = value + '0';
444 else
445 result[(index*2)+1] = value - 10 + 'a';
446 }
448 result[EXPANDED_DIGEST_LENGTH] = 0;
449 return NS_OK;
450 }
452 nsresult
453 nsHttpDigestAuth::CalculateHA1(const nsAFlatCString & username,
454 const nsAFlatCString & password,
455 const nsAFlatCString & realm,
456 uint16_t algorithm,
457 const nsAFlatCString & nonce,
458 const nsAFlatCString & cnonce,
459 char * result)
460 {
461 int16_t len = username.Length() + password.Length() + realm.Length() + 2;
462 if (algorithm & ALGO_MD5_SESS) {
463 int16_t exlen = EXPANDED_DIGEST_LENGTH + nonce.Length() + cnonce.Length() + 2;
464 if (exlen > len)
465 len = exlen;
466 }
468 nsAutoCString contents;
469 contents.SetCapacity(len + 1);
471 contents.Assign(username);
472 contents.Append(':');
473 contents.Append(realm);
474 contents.Append(':');
475 contents.Append(password);
477 nsresult rv;
478 rv = MD5Hash(contents.get(), contents.Length());
479 if (NS_FAILED(rv))
480 return rv;
482 if (algorithm & ALGO_MD5_SESS) {
483 char part1[EXPANDED_DIGEST_LENGTH+1];
484 ExpandToHex(mHashBuf, part1);
486 contents.Assign(part1, EXPANDED_DIGEST_LENGTH);
487 contents.Append(':');
488 contents.Append(nonce);
489 contents.Append(':');
490 contents.Append(cnonce);
492 rv = MD5Hash(contents.get(), contents.Length());
493 if (NS_FAILED(rv))
494 return rv;
495 }
497 return ExpandToHex(mHashBuf, result);
498 }
500 nsresult
501 nsHttpDigestAuth::CalculateHA2(const nsAFlatCString & method,
502 const nsAFlatCString & path,
503 uint16_t qop,
504 const char * bodyDigest,
505 char * result)
506 {
507 uint16_t methodLen = method.Length();
508 uint32_t pathLen = path.Length();
509 uint32_t len = methodLen + pathLen + 1;
511 if (qop & QOP_AUTH_INT) {
512 len += EXPANDED_DIGEST_LENGTH + 1;
513 }
515 nsAutoCString contents;
516 contents.SetCapacity(len);
518 contents.Assign(method);
519 contents.Append(':');
520 contents.Append(path);
522 if (qop & QOP_AUTH_INT) {
523 contents.Append(':');
524 contents.Append(bodyDigest, EXPANDED_DIGEST_LENGTH);
525 }
527 nsresult rv = MD5Hash(contents.get(), contents.Length());
528 if (NS_SUCCEEDED(rv))
529 rv = ExpandToHex(mHashBuf, result);
530 return rv;
531 }
533 nsresult
534 nsHttpDigestAuth::ParseChallenge(const char * challenge,
535 nsACString & realm,
536 nsACString & domain,
537 nsACString & nonce,
538 nsACString & opaque,
539 bool * stale,
540 uint16_t * algorithm,
541 uint16_t * qop)
542 {
543 // put an absurd, but maximum, length cap on the challenge so
544 // that calculations are 32 bit safe
545 if (strlen(challenge) > 16000000) {
546 return NS_ERROR_INVALID_ARG;
547 }
549 const char *p = challenge + 7; // first 7 characters are "Digest "
551 *stale = false;
552 *algorithm = ALGO_MD5; // default is MD5
553 *qop = 0;
555 for (;;) {
556 while (*p && (*p == ',' || nsCRT::IsAsciiSpace(*p)))
557 ++p;
558 if (!*p)
559 break;
561 // name
562 int32_t nameStart = (p - challenge);
563 while (*p && !nsCRT::IsAsciiSpace(*p) && *p != '=')
564 ++p;
565 if (!*p)
566 return NS_ERROR_INVALID_ARG;
567 int32_t nameLength = (p - challenge) - nameStart;
569 while (*p && (nsCRT::IsAsciiSpace(*p) || *p == '='))
570 ++p;
571 if (!*p)
572 return NS_ERROR_INVALID_ARG;
574 bool quoted = false;
575 if (*p == '"') {
576 ++p;
577 quoted = true;
578 }
580 // value
581 int32_t valueStart = (p - challenge);
582 int32_t valueLength = 0;
583 if (quoted) {
584 while (*p && *p != '"')
585 ++p;
586 if (*p != '"')
587 return NS_ERROR_INVALID_ARG;
588 valueLength = (p - challenge) - valueStart;
589 ++p;
590 } else {
591 while (*p && !nsCRT::IsAsciiSpace(*p) && *p != ',')
592 ++p;
593 valueLength = (p - challenge) - valueStart;
594 }
596 // extract information
597 if (nameLength == 5 &&
598 nsCRT::strncasecmp(challenge+nameStart, "realm", 5) == 0)
599 {
600 realm.Assign(challenge+valueStart, valueLength);
601 }
602 else if (nameLength == 6 &&
603 nsCRT::strncasecmp(challenge+nameStart, "domain", 6) == 0)
604 {
605 domain.Assign(challenge+valueStart, valueLength);
606 }
607 else if (nameLength == 5 &&
608 nsCRT::strncasecmp(challenge+nameStart, "nonce", 5) == 0)
609 {
610 nonce.Assign(challenge+valueStart, valueLength);
611 }
612 else if (nameLength == 6 &&
613 nsCRT::strncasecmp(challenge+nameStart, "opaque", 6) == 0)
614 {
615 opaque.Assign(challenge+valueStart, valueLength);
616 }
617 else if (nameLength == 5 &&
618 nsCRT::strncasecmp(challenge+nameStart, "stale", 5) == 0)
619 {
620 if (nsCRT::strncasecmp(challenge+valueStart, "true", 4) == 0)
621 *stale = true;
622 else
623 *stale = false;
624 }
625 else if (nameLength == 9 &&
626 nsCRT::strncasecmp(challenge+nameStart, "algorithm", 9) == 0)
627 {
628 // we want to clear the default, so we use = not |= here
629 *algorithm = ALGO_SPECIFIED;
630 if (valueLength == 3 &&
631 nsCRT::strncasecmp(challenge+valueStart, "MD5", 3) == 0)
632 *algorithm |= ALGO_MD5;
633 else if (valueLength == 8 &&
634 nsCRT::strncasecmp(challenge+valueStart, "MD5-sess", 8) == 0)
635 *algorithm |= ALGO_MD5_SESS;
636 }
637 else if (nameLength == 3 &&
638 nsCRT::strncasecmp(challenge+nameStart, "qop", 3) == 0)
639 {
640 int32_t ipos = valueStart;
641 while (ipos < valueStart+valueLength) {
642 while (ipos < valueStart+valueLength &&
643 (nsCRT::IsAsciiSpace(challenge[ipos]) ||
644 challenge[ipos] == ','))
645 ipos++;
646 int32_t algostart = ipos;
647 while (ipos < valueStart+valueLength &&
648 !nsCRT::IsAsciiSpace(challenge[ipos]) &&
649 challenge[ipos] != ',')
650 ipos++;
651 if ((ipos - algostart) == 4 &&
652 nsCRT::strncasecmp(challenge+algostart, "auth", 4) == 0)
653 *qop |= QOP_AUTH;
654 else if ((ipos - algostart) == 8 &&
655 nsCRT::strncasecmp(challenge+algostart, "auth-int", 8) == 0)
656 *qop |= QOP_AUTH_INT;
657 }
658 }
659 }
660 return NS_OK;
661 }
663 nsresult
664 nsHttpDigestAuth::AppendQuotedString(const nsACString & value,
665 nsACString & aHeaderLine)
666 {
667 nsAutoCString quoted;
668 nsACString::const_iterator s, e;
669 value.BeginReading(s);
670 value.EndReading(e);
672 //
673 // Encode string according to RFC 2616 quoted-string production
674 //
675 quoted.Append('"');
676 for ( ; s != e; ++s) {
677 //
678 // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
679 //
680 if (*s <= 31 || *s == 127) {
681 return NS_ERROR_FAILURE;
682 }
684 // Escape two syntactically significant characters
685 if (*s == '"' || *s == '\\') {
686 quoted.Append('\\');
687 }
689 quoted.Append(*s);
690 }
691 // FIXME: bug 41489
692 // We should RFC2047-encode non-Latin-1 values according to spec
693 quoted.Append('"');
694 aHeaderLine.Append(quoted);
695 return NS_OK;
696 }
698 } // namespace mozilla::net
699 } // namespace mozilla
701 // vim: ts=2 sw=2