Fossil SCM
When connecting via a proxy, use the server's own host name, without its port, in the CONNECT request, TLS server name, certificate check etc.
Commit
33118a37bd6c360ee77e93dc7e495a15b781fd4e2b1be97ba0cf012d309a4bc8
Parent
6753e74e26c9167…
2 files changed
+25
-12
+3
+25
-12
| --- src/http_ssl.c | ||
| +++ src/http_ssl.c | ||
| @@ -379,22 +379,35 @@ | ||
| 379 | 379 | BIO_free_all(iBio); |
| 380 | 380 | iBio = NULL; |
| 381 | 381 | } |
| 382 | 382 | socket_close(); |
| 383 | 383 | } |
| 384 | + | |
| 385 | +/* | |
| 386 | +** Return the host name of the server that pUrlData connects to, without | |
| 387 | +** a port number. When the connection goes through a proxy, | |
| 388 | +** pUrlData->name holds the proxy hostname, and the server hostname is in | |
| 389 | +** pUrlData->proxyOrigName. | |
| 390 | +*/ | |
| 391 | +static const char *ssl_server_name(const UrlData *pUrlData){ | |
| 392 | + return pUrlData->useProxy ? pUrlData->proxyOrigName : pUrlData->name; | |
| 393 | +} | |
| 384 | 394 | |
| 385 | 395 | /* See RFC2817 for details */ |
| 386 | 396 | static int establish_proxy_tunnel(UrlData *pUrlData, BIO *bio){ |
| 387 | 397 | int rc, httpVerMin; |
| 388 | 398 | char *bbuf; |
| 389 | 399 | Blob snd, reply; |
| 390 | 400 | int done=0,end=0; |
| 401 | + const char *zHost = ssl_server_name(pUrlData); | |
| 402 | + const char *zLb = strchr(zHost, ':') ? "[" : ""; /* IPv6 brackets */ | |
| 403 | + const char *zRb = zLb[0] ? "]" : ""; | |
| 391 | 404 | blob_zero(&snd); |
| 392 | - blob_appendf(&snd, "CONNECT %s:%d HTTP/1.1\r\n", pUrlData->hostname, | |
| 393 | - pUrlData->proxyOrigPort); | |
| 394 | - blob_appendf(&snd, "Host: %s:%d\r\n", | |
| 395 | - pUrlData->hostname, pUrlData->proxyOrigPort); | |
| 405 | + blob_appendf(&snd, "CONNECT %s%s%s:%d HTTP/1.1\r\n", | |
| 406 | + zLb, zHost, zRb, pUrlData->proxyOrigPort); | |
| 407 | + blob_appendf(&snd, "Host: %s%s%s:%d\r\n", | |
| 408 | + zLb, zHost, zRb, pUrlData->proxyOrigPort); | |
| 396 | 409 | if( pUrlData->proxyAuth ){ |
| 397 | 410 | blob_appendf(&snd, "Proxy-Authorization: %s\r\n", pUrlData->proxyAuth); |
| 398 | 411 | } |
| 399 | 412 | blob_append(&snd, "Proxy-Connection: keep-alive\r\n", -1); |
| 400 | 413 | blob_appendf(&snd, "User-Agent: %s\r\n", get_user_agent()); |
| @@ -507,11 +520,11 @@ | ||
| 507 | 520 | BIO_push(iBio, sBio); |
| 508 | 521 | BIO_set_ssl(sBio, ssl, BIO_NOCLOSE); |
| 509 | 522 | BIO_set_ssl_mode(iBio, 1); |
| 510 | 523 | BIO_get_ssl(iBio, &ssl); |
| 511 | 524 | |
| 512 | - zRemoteHost = pUrlData->useProxy ? pUrlData->hostname : pUrlData->name; | |
| 525 | + zRemoteHost = ssl_server_name(pUrlData); | |
| 513 | 526 | isIpAddr = ssl_host_is_ip_address(zRemoteHost); |
| 514 | 527 | |
| 515 | 528 | #if (SSLEAY_VERSION_NUMBER >= 0x00908070) && !defined(OPENSSL_NO_TLSEXT) |
| 516 | 529 | if( !isIpAddr && !ssl_host_is_non_ascii(zRemoteHost) |
| 517 | 530 | && !SSL_set_tlsext_host_name(ssl, zRemoteHost) |
| @@ -606,11 +619,11 @@ | ||
| 606 | 619 | BIO_printf(mem, "\n sha256: %s", zHash); |
| 607 | 620 | desclen = BIO_get_mem_data(mem, &desc); |
| 608 | 621 | |
| 609 | 622 | prompt = mprintf("Unable to verify SSL cert from %s\n%.*s\n" |
| 610 | 623 | "accept this cert and continue (y/N/fingerprint)? ", |
| 611 | - pUrlData->name, desclen, desc); | |
| 624 | + zRemoteHost, desclen, desc); | |
| 612 | 625 | BIO_free(mem); |
| 613 | 626 | |
| 614 | 627 | prompt_user(prompt, &ans); |
| 615 | 628 | free(prompt); |
| 616 | 629 | cReply = blob_str(&ans)[0]; |
| @@ -639,34 +652,34 @@ | ||
| 639 | 652 | return 0; |
| 640 | 653 | } |
| 641 | 654 | |
| 642 | 655 | /* |
| 643 | 656 | ** Remember that the cert with the given hash is acceptable for |
| 644 | -** use with pUrlData->name. | |
| 657 | +** use with the server that pUrlData connects to. | |
| 645 | 658 | */ |
| 646 | 659 | LOCAL void ssl_remember_certificate_exception( |
| 647 | 660 | UrlData *pUrlData, |
| 648 | 661 | const char *zHash |
| 649 | 662 | ){ |
| 650 | - db_set_mprintf(zHash, 1, "cert:%s", pUrlData->name); | |
| 663 | + db_set_mprintf(zHash, 1, "cert:%s", ssl_server_name(pUrlData)); | |
| 651 | 664 | } |
| 652 | 665 | |
| 653 | 666 | /* |
| 654 | 667 | ** Return true if the there exists a certificate exception for |
| 655 | -** pUrlData->name that matches the hash. | |
| 668 | +** the server that pUrlData connects to that matches the hash. | |
| 656 | 669 | */ |
| 657 | 670 | LOCAL int ssl_certificate_exception_exists( |
| 658 | 671 | UrlData *pUrlData, |
| 659 | 672 | const char *zHash |
| 660 | 673 | ){ |
| 661 | 674 | char *zName, *zValue; |
| 662 | - if( fossil_strcmp(sException.zHost,pUrlData->name)==0 | |
| 675 | + if( fossil_strcmp(sException.zHost,ssl_server_name(pUrlData))==0 | |
| 663 | 676 | && fossil_strcmp(sException.zHash,zHash)==0 |
| 664 | 677 | ){ |
| 665 | 678 | return 1; |
| 666 | 679 | } |
| 667 | - zName = mprintf("cert:%s", pUrlData->name); | |
| 680 | + zName = mprintf("cert:%s", ssl_server_name(pUrlData)); | |
| 668 | 681 | zValue = db_get(zName,0); |
| 669 | 682 | fossil_free(zName); |
| 670 | 683 | return zValue!=0 && strcmp(zHash,zValue)==0; |
| 671 | 684 | } |
| 672 | 685 | |
| @@ -676,11 +689,11 @@ | ||
| 676 | 689 | LOCAL void ssl_one_time_exception( |
| 677 | 690 | UrlData *pUrlData, |
| 678 | 691 | const char *zHash |
| 679 | 692 | ){ |
| 680 | 693 | fossil_free(sException.zHost); |
| 681 | - sException.zHost = fossil_strdup(pUrlData->name); | |
| 694 | + sException.zHost = fossil_strdup(ssl_server_name(pUrlData)); | |
| 682 | 695 | fossil_free(sException.zHash); |
| 683 | 696 | sException.zHash = fossil_strdup(zHash); |
| 684 | 697 | } |
| 685 | 698 | |
| 686 | 699 | /* |
| 687 | 700 |
| --- src/http_ssl.c | |
| +++ src/http_ssl.c | |
| @@ -379,22 +379,35 @@ | |
| 379 | BIO_free_all(iBio); |
| 380 | iBio = NULL; |
| 381 | } |
| 382 | socket_close(); |
| 383 | } |
| 384 | |
| 385 | /* See RFC2817 for details */ |
| 386 | static int establish_proxy_tunnel(UrlData *pUrlData, BIO *bio){ |
| 387 | int rc, httpVerMin; |
| 388 | char *bbuf; |
| 389 | Blob snd, reply; |
| 390 | int done=0,end=0; |
| 391 | blob_zero(&snd); |
| 392 | blob_appendf(&snd, "CONNECT %s:%d HTTP/1.1\r\n", pUrlData->hostname, |
| 393 | pUrlData->proxyOrigPort); |
| 394 | blob_appendf(&snd, "Host: %s:%d\r\n", |
| 395 | pUrlData->hostname, pUrlData->proxyOrigPort); |
| 396 | if( pUrlData->proxyAuth ){ |
| 397 | blob_appendf(&snd, "Proxy-Authorization: %s\r\n", pUrlData->proxyAuth); |
| 398 | } |
| 399 | blob_append(&snd, "Proxy-Connection: keep-alive\r\n", -1); |
| 400 | blob_appendf(&snd, "User-Agent: %s\r\n", get_user_agent()); |
| @@ -507,11 +520,11 @@ | |
| 507 | BIO_push(iBio, sBio); |
| 508 | BIO_set_ssl(sBio, ssl, BIO_NOCLOSE); |
| 509 | BIO_set_ssl_mode(iBio, 1); |
| 510 | BIO_get_ssl(iBio, &ssl); |
| 511 | |
| 512 | zRemoteHost = pUrlData->useProxy ? pUrlData->hostname : pUrlData->name; |
| 513 | isIpAddr = ssl_host_is_ip_address(zRemoteHost); |
| 514 | |
| 515 | #if (SSLEAY_VERSION_NUMBER >= 0x00908070) && !defined(OPENSSL_NO_TLSEXT) |
| 516 | if( !isIpAddr && !ssl_host_is_non_ascii(zRemoteHost) |
| 517 | && !SSL_set_tlsext_host_name(ssl, zRemoteHost) |
| @@ -606,11 +619,11 @@ | |
| 606 | BIO_printf(mem, "\n sha256: %s", zHash); |
| 607 | desclen = BIO_get_mem_data(mem, &desc); |
| 608 | |
| 609 | prompt = mprintf("Unable to verify SSL cert from %s\n%.*s\n" |
| 610 | "accept this cert and continue (y/N/fingerprint)? ", |
| 611 | pUrlData->name, desclen, desc); |
| 612 | BIO_free(mem); |
| 613 | |
| 614 | prompt_user(prompt, &ans); |
| 615 | free(prompt); |
| 616 | cReply = blob_str(&ans)[0]; |
| @@ -639,34 +652,34 @@ | |
| 639 | return 0; |
| 640 | } |
| 641 | |
| 642 | /* |
| 643 | ** Remember that the cert with the given hash is acceptable for |
| 644 | ** use with pUrlData->name. |
| 645 | */ |
| 646 | LOCAL void ssl_remember_certificate_exception( |
| 647 | UrlData *pUrlData, |
| 648 | const char *zHash |
| 649 | ){ |
| 650 | db_set_mprintf(zHash, 1, "cert:%s", pUrlData->name); |
| 651 | } |
| 652 | |
| 653 | /* |
| 654 | ** Return true if the there exists a certificate exception for |
| 655 | ** pUrlData->name that matches the hash. |
| 656 | */ |
| 657 | LOCAL int ssl_certificate_exception_exists( |
| 658 | UrlData *pUrlData, |
| 659 | const char *zHash |
| 660 | ){ |
| 661 | char *zName, *zValue; |
| 662 | if( fossil_strcmp(sException.zHost,pUrlData->name)==0 |
| 663 | && fossil_strcmp(sException.zHash,zHash)==0 |
| 664 | ){ |
| 665 | return 1; |
| 666 | } |
| 667 | zName = mprintf("cert:%s", pUrlData->name); |
| 668 | zValue = db_get(zName,0); |
| 669 | fossil_free(zName); |
| 670 | return zValue!=0 && strcmp(zHash,zValue)==0; |
| 671 | } |
| 672 | |
| @@ -676,11 +689,11 @@ | |
| 676 | LOCAL void ssl_one_time_exception( |
| 677 | UrlData *pUrlData, |
| 678 | const char *zHash |
| 679 | ){ |
| 680 | fossil_free(sException.zHost); |
| 681 | sException.zHost = fossil_strdup(pUrlData->name); |
| 682 | fossil_free(sException.zHash); |
| 683 | sException.zHash = fossil_strdup(zHash); |
| 684 | } |
| 685 | |
| 686 | /* |
| 687 |
| --- src/http_ssl.c | |
| +++ src/http_ssl.c | |
| @@ -379,22 +379,35 @@ | |
| 379 | BIO_free_all(iBio); |
| 380 | iBio = NULL; |
| 381 | } |
| 382 | socket_close(); |
| 383 | } |
| 384 | |
| 385 | /* |
| 386 | ** Return the host name of the server that pUrlData connects to, without |
| 387 | ** a port number. When the connection goes through a proxy, |
| 388 | ** pUrlData->name holds the proxy hostname, and the server hostname is in |
| 389 | ** pUrlData->proxyOrigName. |
| 390 | */ |
| 391 | static const char *ssl_server_name(const UrlData *pUrlData){ |
| 392 | return pUrlData->useProxy ? pUrlData->proxyOrigName : pUrlData->name; |
| 393 | } |
| 394 | |
| 395 | /* See RFC2817 for details */ |
| 396 | static int establish_proxy_tunnel(UrlData *pUrlData, BIO *bio){ |
| 397 | int rc, httpVerMin; |
| 398 | char *bbuf; |
| 399 | Blob snd, reply; |
| 400 | int done=0,end=0; |
| 401 | const char *zHost = ssl_server_name(pUrlData); |
| 402 | const char *zLb = strchr(zHost, ':') ? "[" : ""; /* IPv6 brackets */ |
| 403 | const char *zRb = zLb[0] ? "]" : ""; |
| 404 | blob_zero(&snd); |
| 405 | blob_appendf(&snd, "CONNECT %s%s%s:%d HTTP/1.1\r\n", |
| 406 | zLb, zHost, zRb, pUrlData->proxyOrigPort); |
| 407 | blob_appendf(&snd, "Host: %s%s%s:%d\r\n", |
| 408 | zLb, zHost, zRb, pUrlData->proxyOrigPort); |
| 409 | if( pUrlData->proxyAuth ){ |
| 410 | blob_appendf(&snd, "Proxy-Authorization: %s\r\n", pUrlData->proxyAuth); |
| 411 | } |
| 412 | blob_append(&snd, "Proxy-Connection: keep-alive\r\n", -1); |
| 413 | blob_appendf(&snd, "User-Agent: %s\r\n", get_user_agent()); |
| @@ -507,11 +520,11 @@ | |
| 520 | BIO_push(iBio, sBio); |
| 521 | BIO_set_ssl(sBio, ssl, BIO_NOCLOSE); |
| 522 | BIO_set_ssl_mode(iBio, 1); |
| 523 | BIO_get_ssl(iBio, &ssl); |
| 524 | |
| 525 | zRemoteHost = ssl_server_name(pUrlData); |
| 526 | isIpAddr = ssl_host_is_ip_address(zRemoteHost); |
| 527 | |
| 528 | #if (SSLEAY_VERSION_NUMBER >= 0x00908070) && !defined(OPENSSL_NO_TLSEXT) |
| 529 | if( !isIpAddr && !ssl_host_is_non_ascii(zRemoteHost) |
| 530 | && !SSL_set_tlsext_host_name(ssl, zRemoteHost) |
| @@ -606,11 +619,11 @@ | |
| 619 | BIO_printf(mem, "\n sha256: %s", zHash); |
| 620 | desclen = BIO_get_mem_data(mem, &desc); |
| 621 | |
| 622 | prompt = mprintf("Unable to verify SSL cert from %s\n%.*s\n" |
| 623 | "accept this cert and continue (y/N/fingerprint)? ", |
| 624 | zRemoteHost, desclen, desc); |
| 625 | BIO_free(mem); |
| 626 | |
| 627 | prompt_user(prompt, &ans); |
| 628 | free(prompt); |
| 629 | cReply = blob_str(&ans)[0]; |
| @@ -639,34 +652,34 @@ | |
| 652 | return 0; |
| 653 | } |
| 654 | |
| 655 | /* |
| 656 | ** Remember that the cert with the given hash is acceptable for |
| 657 | ** use with the server that pUrlData connects to. |
| 658 | */ |
| 659 | LOCAL void ssl_remember_certificate_exception( |
| 660 | UrlData *pUrlData, |
| 661 | const char *zHash |
| 662 | ){ |
| 663 | db_set_mprintf(zHash, 1, "cert:%s", ssl_server_name(pUrlData)); |
| 664 | } |
| 665 | |
| 666 | /* |
| 667 | ** Return true if the there exists a certificate exception for |
| 668 | ** the server that pUrlData connects to that matches the hash. |
| 669 | */ |
| 670 | LOCAL int ssl_certificate_exception_exists( |
| 671 | UrlData *pUrlData, |
| 672 | const char *zHash |
| 673 | ){ |
| 674 | char *zName, *zValue; |
| 675 | if( fossil_strcmp(sException.zHost,ssl_server_name(pUrlData))==0 |
| 676 | && fossil_strcmp(sException.zHash,zHash)==0 |
| 677 | ){ |
| 678 | return 1; |
| 679 | } |
| 680 | zName = mprintf("cert:%s", ssl_server_name(pUrlData)); |
| 681 | zValue = db_get(zName,0); |
| 682 | fossil_free(zName); |
| 683 | return zValue!=0 && strcmp(zHash,zValue)==0; |
| 684 | } |
| 685 | |
| @@ -676,11 +689,11 @@ | |
| 689 | LOCAL void ssl_one_time_exception( |
| 690 | UrlData *pUrlData, |
| 691 | const char *zHash |
| 692 | ){ |
| 693 | fossil_free(sException.zHost); |
| 694 | sException.zHost = fossil_strdup(ssl_server_name(pUrlData)); |
| 695 | fossil_free(sException.zHash); |
| 696 | sException.zHash = fossil_strdup(zHash); |
| 697 | } |
| 698 | |
| 699 | /* |
| 700 |
+3
| --- src/url.c | ||
| +++ src/url.c | ||
| @@ -63,10 +63,11 @@ | ||
| 63 | 63 | char *subpath; /* Secondary HTTP request path for ssh: and file: */ |
| 64 | 64 | char *pwConfig; /* CONFIG table entry that gave us the password */ |
| 65 | 65 | unsigned flags; /* Boolean flags controlling URL processing */ |
| 66 | 66 | int useProxy; /* Used to remember that a proxy is in use */ |
| 67 | 67 | int proxyOrigPort; /* Tunneled port number for https through proxy */ |
| 68 | + char *proxyOrigName; /* Tunneled host name for https through proxy */ | |
| 68 | 69 | char *proxyUrlPath; /* Remember path when proxy is use */ |
| 69 | 70 | char *proxyUrlCanonical; /* Remember canonical path when proxy is use */ |
| 70 | 71 | }; |
| 71 | 72 | #endif /* INTERFACE */ |
| 72 | 73 | |
| @@ -627,10 +628,11 @@ | ||
| 627 | 628 | } |
| 628 | 629 | if( zProxy && zProxy[0] && !is_false(zProxy) |
| 629 | 630 | && !g.url.isSsh && !g.url.isFile ){ |
| 630 | 631 | char *zOriginalUrl = g.url.canonical; |
| 631 | 632 | char *zOriginalHost = g.url.hostname; |
| 633 | + char *zOriginalName = g.url.name; | |
| 632 | 634 | int fOriginalIsHttps = g.url.isHttps; |
| 633 | 635 | char *zOriginalUser = g.url.user; |
| 634 | 636 | char *zOriginalPasswd = g.url.passwd; |
| 635 | 637 | char *zOriginalUrlPath = g.url.path; |
| 636 | 638 | int iOriginalPort = g.url.port; |
| @@ -652,10 +654,11 @@ | ||
| 652 | 654 | g.url.isHttps = fOriginalIsHttps; |
| 653 | 655 | g.url.useProxy = 1; |
| 654 | 656 | g.url.proxyUrlCanonical = zOriginalUrl;; |
| 655 | 657 | g.url.proxyUrlPath = zOriginalUrlPath; |
| 656 | 658 | g.url.proxyOrigPort = iOriginalPort; |
| 659 | + g.url.proxyOrigName = zOriginalName; | |
| 657 | 660 | g.url.flags = uOriginalFlags; |
| 658 | 661 | } |
| 659 | 662 | } |
| 660 | 663 | |
| 661 | 664 | #if INTERFACE |
| 662 | 665 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -63,10 +63,11 @@ | |
| 63 | char *subpath; /* Secondary HTTP request path for ssh: and file: */ |
| 64 | char *pwConfig; /* CONFIG table entry that gave us the password */ |
| 65 | unsigned flags; /* Boolean flags controlling URL processing */ |
| 66 | int useProxy; /* Used to remember that a proxy is in use */ |
| 67 | int proxyOrigPort; /* Tunneled port number for https through proxy */ |
| 68 | char *proxyUrlPath; /* Remember path when proxy is use */ |
| 69 | char *proxyUrlCanonical; /* Remember canonical path when proxy is use */ |
| 70 | }; |
| 71 | #endif /* INTERFACE */ |
| 72 | |
| @@ -627,10 +628,11 @@ | |
| 627 | } |
| 628 | if( zProxy && zProxy[0] && !is_false(zProxy) |
| 629 | && !g.url.isSsh && !g.url.isFile ){ |
| 630 | char *zOriginalUrl = g.url.canonical; |
| 631 | char *zOriginalHost = g.url.hostname; |
| 632 | int fOriginalIsHttps = g.url.isHttps; |
| 633 | char *zOriginalUser = g.url.user; |
| 634 | char *zOriginalPasswd = g.url.passwd; |
| 635 | char *zOriginalUrlPath = g.url.path; |
| 636 | int iOriginalPort = g.url.port; |
| @@ -652,10 +654,11 @@ | |
| 652 | g.url.isHttps = fOriginalIsHttps; |
| 653 | g.url.useProxy = 1; |
| 654 | g.url.proxyUrlCanonical = zOriginalUrl;; |
| 655 | g.url.proxyUrlPath = zOriginalUrlPath; |
| 656 | g.url.proxyOrigPort = iOriginalPort; |
| 657 | g.url.flags = uOriginalFlags; |
| 658 | } |
| 659 | } |
| 660 | |
| 661 | #if INTERFACE |
| 662 |
| --- src/url.c | |
| +++ src/url.c | |
| @@ -63,10 +63,11 @@ | |
| 63 | char *subpath; /* Secondary HTTP request path for ssh: and file: */ |
| 64 | char *pwConfig; /* CONFIG table entry that gave us the password */ |
| 65 | unsigned flags; /* Boolean flags controlling URL processing */ |
| 66 | int useProxy; /* Used to remember that a proxy is in use */ |
| 67 | int proxyOrigPort; /* Tunneled port number for https through proxy */ |
| 68 | char *proxyOrigName; /* Tunneled host name for https through proxy */ |
| 69 | char *proxyUrlPath; /* Remember path when proxy is use */ |
| 70 | char *proxyUrlCanonical; /* Remember canonical path when proxy is use */ |
| 71 | }; |
| 72 | #endif /* INTERFACE */ |
| 73 | |
| @@ -627,10 +628,11 @@ | |
| 628 | } |
| 629 | if( zProxy && zProxy[0] && !is_false(zProxy) |
| 630 | && !g.url.isSsh && !g.url.isFile ){ |
| 631 | char *zOriginalUrl = g.url.canonical; |
| 632 | char *zOriginalHost = g.url.hostname; |
| 633 | char *zOriginalName = g.url.name; |
| 634 | int fOriginalIsHttps = g.url.isHttps; |
| 635 | char *zOriginalUser = g.url.user; |
| 636 | char *zOriginalPasswd = g.url.passwd; |
| 637 | char *zOriginalUrlPath = g.url.path; |
| 638 | int iOriginalPort = g.url.port; |
| @@ -652,10 +654,11 @@ | |
| 654 | g.url.isHttps = fOriginalIsHttps; |
| 655 | g.url.useProxy = 1; |
| 656 | g.url.proxyUrlCanonical = zOriginalUrl;; |
| 657 | g.url.proxyUrlPath = zOriginalUrlPath; |
| 658 | g.url.proxyOrigPort = iOriginalPort; |
| 659 | g.url.proxyOrigName = zOriginalName; |
| 660 | g.url.flags = uOriginalFlags; |
| 661 | } |
| 662 | } |
| 663 | |
| 664 | #if INTERFACE |
| 665 |