From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- netwerk/test/unit/CA.key.pem | 30 + netwerk/test/unit/client_cert_chooser.js | 27 + netwerk/test/unit/client_cert_chooser.manifest | 2 + netwerk/test/unit/data/cookies_v10.sqlite | Bin 0 -> 131072 bytes netwerk/test/unit/data/image.png | Bin 0 -> 102591 bytes netwerk/test/unit/data/signed_win.exe | Bin 0 -> 61064 bytes netwerk/test/unit/data/system_root.lnk | Bin 0 -> 1677 bytes netwerk/test/unit/data/test_psl.txt | 98 + netwerk/test/unit/data/test_readline1.txt | 0 netwerk/test/unit/data/test_readline2.txt | 1 + netwerk/test/unit/data/test_readline3.txt | 3 + netwerk/test/unit/data/test_readline4.txt | 3 + netwerk/test/unit/data/test_readline5.txt | 1 + netwerk/test/unit/data/test_readline6.txt | 1 + netwerk/test/unit/data/test_readline7.txt | 2 + netwerk/test/unit/data/test_readline8.txt | 1 + netwerk/test/unit/head_cache.js | 167 ++ netwerk/test/unit/head_cache2.js | 428 ++++ netwerk/test/unit/head_channels.js | 456 +++++ netwerk/test/unit/head_cookies.js | 973 +++++++++ netwerk/test/unit/head_http3.js | 105 + netwerk/test/unit/head_trr.js | 364 ++++ netwerk/test/unit/http2-ca.pem | 18 + netwerk/test/unit/http2-ca.pem.certspec | 4 + netwerk/test/unit/moz.build | 8 + netwerk/test/unit/perftest.ini | 1 + netwerk/test/unit/socks_client_subprocess.js | 101 + netwerk/test/unit/test_1073747.js | 42 + netwerk/test/unit/test_304_responses.js | 106 + netwerk/test/unit/test_307_redirect.js | 96 + netwerk/test/unit/test_421.js | 64 + netwerk/test/unit/test_MIME_params.js | 784 +++++++ netwerk/test/unit/test_NetUtil.js | 818 ++++++++ netwerk/test/unit/test_SuperfluousAuth.js | 99 + netwerk/test/unit/test_URIs.js | 1042 ++++++++++ netwerk/test/unit/test_URIs2.js | 906 +++++++++ netwerk/test/unit/test_XHR_redirects.js | 273 +++ netwerk/test/unit/test_about_networking.js | 121 ++ netwerk/test/unit/test_about_protocol.js | 57 + netwerk/test/unit/test_aboutblank.js | 32 + netwerk/test/unit/test_addr_in_use_error.js | 36 + netwerk/test/unit/test_alt-data_closeWithStatus.js | 174 ++ netwerk/test/unit/test_alt-data_cross_process.js | 143 ++ netwerk/test/unit/test_alt-data_overwrite.js | 203 ++ netwerk/test/unit/test_alt-data_simple.js | 196 ++ netwerk/test/unit/test_alt-data_stream.js | 153 ++ netwerk/test/unit/test_alt-data_too_big.js | 113 ++ netwerk/test/unit/test_altsvc.js | 519 +++++ netwerk/test/unit/test_altsvc_http3.js | 496 +++++ netwerk/test/unit/test_altsvc_pref.js | 139 ++ netwerk/test/unit/test_anonymous-coalescing.js | 186 ++ netwerk/test/unit/test_auth_dialog_permission.js | 283 +++ netwerk/test/unit/test_auth_jar.js | 97 + netwerk/test/unit/test_auth_proxy.js | 465 +++++ netwerk/test/unit/test_authentication.js | 754 +++++++ netwerk/test/unit/test_authpromptwrapper.js | 233 +++ netwerk/test/unit/test_backgroundfilesaver.js | 775 +++++++ netwerk/test/unit/test_be_conservative.js | 247 +++ .../unit/test_be_conservative_error_handling.js | 242 +++ netwerk/test/unit/test_blob_channelname.js | 42 + netwerk/test/unit/test_brotli_http.js | 44 + netwerk/test/unit/test_bug1064258.js | 142 ++ netwerk/test/unit/test_bug1177909.js | 194 ++ netwerk/test/unit/test_bug1195415.js | 163 ++ netwerk/test/unit/test_bug1218029.js | 116 ++ netwerk/test/unit/test_bug1279246.js | 98 + netwerk/test/unit/test_bug1312774_http1.js | 153 ++ netwerk/test/unit/test_bug1312782_http1.js | 206 ++ netwerk/test/unit/test_bug1355539_http1.js | 207 ++ netwerk/test/unit/test_bug1378385_http1.js | 202 ++ netwerk/test/unit/test_bug1411316_http1.js | 117 ++ netwerk/test/unit/test_bug1527293.js | 92 + netwerk/test/unit/test_bug1683176.js | 104 + netwerk/test/unit/test_bug203271.js | 248 +++ netwerk/test/unit/test_bug248970_cache.js | 168 ++ netwerk/test/unit/test_bug248970_cookie.js | 150 ++ netwerk/test/unit/test_bug261425.js | 37 + netwerk/test/unit/test_bug263127.js | 58 + netwerk/test/unit/test_bug282432.js | 38 + netwerk/test/unit/test_bug321706.js | 12 + netwerk/test/unit/test_bug331825.js | 41 + netwerk/test/unit/test_bug336501.js | 26 + netwerk/test/unit/test_bug337744.js | 126 ++ netwerk/test/unit/test_bug365133.js | 131 ++ netwerk/test/unit/test_bug368702.js | 153 ++ netwerk/test/unit/test_bug369787.js | 71 + netwerk/test/unit/test_bug371473.js | 40 + netwerk/test/unit/test_bug376844.js | 23 + netwerk/test/unit/test_bug376865.js | 23 + netwerk/test/unit/test_bug379034.js | 21 + netwerk/test/unit/test_bug380994.js | 26 + netwerk/test/unit/test_bug388281.js | 42 + netwerk/test/unit/test_bug396389.js | 71 + netwerk/test/unit/test_bug401564.js | 44 + netwerk/test/unit/test_bug411952.js | 53 + netwerk/test/unit/test_bug412457.js | 117 ++ netwerk/test/unit/test_bug412945.js | 42 + netwerk/test/unit/test_bug414122.js | 62 + netwerk/test/unit/test_bug427957.js | 106 + netwerk/test/unit/test_bug429347.js | 75 + netwerk/test/unit/test_bug455311.js | 128 ++ netwerk/test/unit/test_bug464591.js | 100 + netwerk/test/unit/test_bug468426.js | 129 ++ netwerk/test/unit/test_bug468594.js | 176 ++ netwerk/test/unit/test_bug470716.js | 171 ++ netwerk/test/unit/test_bug477578.js | 51 + netwerk/test/unit/test_bug479413.js | 61 + netwerk/test/unit/test_bug479485.js | 65 + netwerk/test/unit/test_bug482601.js | 262 +++ netwerk/test/unit/test_bug482934.js | 189 ++ netwerk/test/unit/test_bug484684.js | 139 ++ netwerk/test/unit/test_bug490095.js | 158 ++ netwerk/test/unit/test_bug504014.js | 74 + netwerk/test/unit/test_bug510359.js | 102 + netwerk/test/unit/test_bug515583.js | 82 + netwerk/test/unit/test_bug526789.js | 287 +++ netwerk/test/unit/test_bug528292.js | 93 + .../unit/test_bug536324_64bit_content_length.js | 64 + netwerk/test/unit/test_bug540566.js | 24 + netwerk/test/unit/test_bug543805.js | 136 ++ netwerk/test/unit/test_bug553970.js | 53 + netwerk/test/unit/test_bug561042.js | 42 + netwerk/test/unit/test_bug561276.js | 64 + netwerk/test/unit/test_bug580508.js | 33 + netwerk/test/unit/test_bug586908.js | 104 + netwerk/test/unit/test_bug596443.js | 115 ++ netwerk/test/unit/test_bug618835.js | 122 ++ netwerk/test/unit/test_bug633743.js | 193 ++ netwerk/test/unit/test_bug650522.js | 34 + netwerk/test/unit/test_bug650995.js | 189 ++ netwerk/test/unit/test_bug652761.js | 19 + netwerk/test/unit/test_bug654926.js | 102 + netwerk/test/unit/test_bug654926_doom_and_read.js | 90 + netwerk/test/unit/test_bug654926_test_seek.js | 76 + netwerk/test/unit/test_bug659569.js | 58 + netwerk/test/unit/test_bug660066.js | 56 + netwerk/test/unit/test_bug667087.js | 32 + netwerk/test/unit/test_bug667818.js | 50 + netwerk/test/unit/test_bug667907.js | 86 + netwerk/test/unit/test_bug669001.js | 179 ++ netwerk/test/unit/test_bug767025.js | 315 +++ netwerk/test/unit/test_bug770243.js | 246 +++ netwerk/test/unit/test_bug812167.js | 141 ++ netwerk/test/unit/test_bug826063.js | 91 + netwerk/test/unit/test_bug856978.js | 135 ++ netwerk/test/unit/test_bug894586.js | 157 ++ netwerk/test/unit/test_bug935499.js | 10 + netwerk/test/unit/test_cache-control_request.js | 447 ++++ netwerk/test/unit/test_cache-entry-id.js | 215 ++ netwerk/test/unit/test_cache2-00-service-get.js | 19 + netwerk/test/unit/test_cache2-01-basic.js | 45 + .../test/unit/test_cache2-01a-basic-readonly.js | 45 + .../test/unit/test_cache2-01b-basic-datasize.js | 49 + .../unit/test_cache2-01c-basic-hasmeta-only.js | 45 + .../test/unit/test_cache2-01d-basic-not-wanted.js | 45 + .../unit/test_cache2-01e-basic-bypass-if-busy.js | 39 + .../unit/test_cache2-01f-basic-openTruncate.js | 24 + .../test/unit/test_cache2-02-open-non-existing.js | 45 + .../test_cache2-03-oncacheentryavail-throws.js | 36 + .../test_cache2-04-oncacheentryavail-throws2x.js | 45 + netwerk/test/unit/test_cache2-05-visit.js | 113 ++ netwerk/test/unit/test_cache2-06-pb-mode.js | 53 + netwerk/test/unit/test_cache2-07-visit-memory.js | 123 ++ netwerk/test/unit/test_cache2-07a-open-memory.js | 81 + .../test_cache2-08-evict-disk-by-memory-storage.js | 25 + .../test/unit/test_cache2-09-evict-disk-by-uri.js | 32 + netwerk/test/unit/test_cache2-10-evict-direct.js | 29 + .../unit/test_cache2-10b-evict-direct-immediate.js | 21 + netwerk/test/unit/test_cache2-11-evict-memory.js | 89 + netwerk/test/unit/test_cache2-12-evict-disk.js | 81 + .../test/unit/test_cache2-13-evict-non-existing.js | 16 + .../test/unit/test_cache2-14-concurent-readers.js | 48 + .../test_cache2-14b-concurent-readers-complete.js | 76 + .../test/unit/test_cache2-15-conditional-304.js | 60 + .../test/unit/test_cache2-16-conditional-200.js | 76 + netwerk/test/unit/test_cache2-17-evict-all.js | 18 + netwerk/test/unit/test_cache2-18-not-valid.js | 38 + netwerk/test/unit/test_cache2-19-range-206.js | 65 + netwerk/test/unit/test_cache2-20-range-200.js | 66 + netwerk/test/unit/test_cache2-21-anon-storage.js | 52 + netwerk/test/unit/test_cache2-22-anon-visit.js | 43 + .../test/unit/test_cache2-23-read-over-chunk.js | 34 + netwerk/test/unit/test_cache2-24-exists.js | 43 + .../test/unit/test_cache2-25-chunk-memory-limit.js | 57 + .../unit/test_cache2-26-no-outputstream-open.js | 36 + .../test/unit/test_cache2-27-force-valid-for.js | 35 + .../test/unit/test_cache2-28-last-access-attrs.js | 46 + netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js | 42 + ...9a-concurrent_read_resumable_entry_size_zero.js | 74 + ...oncurrent_read_non-resumable_entry_size_zero.js | 78 + ..._cache2-29c-concurrent_read_half-interrupted.js | 93 + ...ache2-29d-concurrent_read_half-corrupted-206.js | 93 + ...e2-29e-concurrent_read_half-non-206-response.js | 88 + netwerk/test/unit/test_cache2-30a-entry-pinning.js | 39 + .../unit/test_cache2-30b-pinning-storage-clear.js | 45 + .../unit/test_cache2-30c-pinning-deferred-doom.js | 188 ++ .../unit/test_cache2-30d-pinning-WasEvicted-API.js | 151 ++ netwerk/test/unit/test_cache2-31-visit-all.js | 88 + netwerk/test/unit/test_cache2-32-clear-origin.js | 68 + .../test/unit/test_cacheForOfflineUse_no-store.js | 104 + netwerk/test/unit/test_cache_204_response.js | 60 + netwerk/test/unit/test_cache_jar.js | 104 + netwerk/test/unit/test_cacheflags.js | 436 ++++ netwerk/test/unit/test_captive_portal_service.js | 207 ++ netwerk/test/unit/test_channel_close.js | 68 + netwerk/test/unit/test_channel_priority.js | 97 + netwerk/test/unit/test_chunked_responses.js | 172 ++ netwerk/test/unit/test_compareURIs.js | 60 + netwerk/test/unit/test_compressappend.js | 99 + netwerk/test/unit/test_content_encoding_gzip.js | 210 ++ netwerk/test/unit/test_content_length_underrun.js | 280 +++ netwerk/test/unit/test_content_sniffer.js | 163 ++ netwerk/test/unit/test_cookie_blacklist.js | 39 + netwerk/test/unit/test_cookie_header.js | 113 ++ netwerk/test/unit/test_cookie_ipv6.js | 52 + netwerk/test/unit/test_cookiejars.js | 172 ++ netwerk/test/unit/test_cookiejars_safebrowsing.js | 238 +++ netwerk/test/unit/test_cookies_async_failure.js | 508 +++++ netwerk/test/unit/test_cookies_persistence.js | 76 + netwerk/test/unit/test_cookies_privatebrowsing.js | 139 ++ netwerk/test/unit/test_cookies_profile_close.js | 112 + netwerk/test/unit/test_cookies_read.js | 114 ++ netwerk/test/unit/test_cookies_sync_failure.js | 344 ++++ netwerk/test/unit/test_cookies_thirdparty.js | 162 ++ .../test/unit/test_cookies_thirdparty_session.js | 71 + netwerk/test/unit/test_cookies_upgrade_10.js | 59 + netwerk/test/unit/test_data_protocol.js | 91 + netwerk/test/unit/test_defaultURI.js | 185 ++ netwerk/test/unit/test_disabled_ftp.js | 21 + netwerk/test/unit/test_dns_by_type_resolve.js | 180 ++ netwerk/test/unit/test_dns_cancel.js | 125 ++ netwerk/test/unit/test_dns_disable_ipv4.js | 55 + netwerk/test/unit/test_dns_disable_ipv6.js | 56 + netwerk/test/unit/test_dns_disabled.js | 94 + netwerk/test/unit/test_dns_localredirect.js | 68 + netwerk/test/unit/test_dns_offline.js | 113 ++ netwerk/test/unit/test_dns_onion.js | 82 + netwerk/test/unit/test_dns_originAttributes.js | 99 + netwerk/test/unit/test_dns_override.js | 311 +++ .../test/unit/test_dns_override_for_localhost.js | 92 + netwerk/test/unit/test_dns_proxy_bypass.js | 82 + netwerk/test/unit/test_dns_service.js | 68 + netwerk/test/unit/test_domain_eviction.js | 185 ++ netwerk/test/unit/test_doomentry.js | 108 + netwerk/test/unit/test_duplicate_headers.js | 558 +++++ netwerk/test/unit/test_event_sink.js | 195 ++ netwerk/test/unit/test_eviction.js | 252 +++ .../unit/test_extract_charset_from_content_type.js | 245 +++ .../unit/test_fallback_no-cache-entry_canceled.js | 131 ++ .../unit/test_fallback_no-cache-entry_passing.js | 130 ++ ...llback_redirect-to-different-origin_canceled.js | 133 ++ ...allback_redirect-to-different-origin_passing.js | 132 ++ .../unit/test_fallback_request-error_canceled.js | 140 ++ .../unit/test_fallback_request-error_passing.js | 136 ++ .../unit/test_fallback_response-error_canceled.js | 133 ++ .../unit/test_fallback_response-error_passing.js | 132 ++ netwerk/test/unit/test_file_protocol.js | 280 +++ netwerk/test/unit/test_filestreams.js | 306 +++ netwerk/test/unit/test_freshconnection.js | 30 + netwerk/test/unit/test_getHost.js | 63 + netwerk/test/unit/test_gre_resources.js | 32 + netwerk/test/unit/test_gzipped_206.js | 167 ++ netwerk/test/unit/test_head.js | 169 ++ .../unit/test_head_request_no_response_body.js | 76 + netwerk/test/unit/test_header_Accept-Language.js | 101 + .../test/unit/test_header_Accept-Language_case.js | 53 + netwerk/test/unit/test_header_Server_Timing.js | 67 + netwerk/test/unit/test_headers.js | 175 ++ netwerk/test/unit/test_hostnameIsLocalIPAddress.js | 41 + .../test/unit/test_hostnameIsSharedIPAddress.js | 21 + netwerk/test/unit/test_http1-proxy.js | 229 +++ netwerk/test/unit/test_http2-proxy.js | 615 ++++++ netwerk/test/unit/test_http2.js | 1442 +++++++++++++ netwerk/test/unit/test_http3.js | 569 ++++++ netwerk/test/unit/test_http3_421.js | 175 ++ netwerk/test/unit/test_http3_alt_svc.js | 129 ++ .../test/unit/test_http3_error_before_connect.js | 123 ++ netwerk/test/unit/test_http3_fast_fallback.js | 597 ++++++ netwerk/test/unit/test_http3_fatal_stream_error.js | 149 ++ netwerk/test/unit/test_http3_large_post.js | 101 + netwerk/test/unit/test_http3_perf.js | 262 +++ .../test/unit/test_http3_server_not_existing.js | 123 ++ netwerk/test/unit/test_http3_trans_close.js | 84 + netwerk/test/unit/test_httpResponseTimeout.js | 162 ++ netwerk/test/unit/test_http_headers.js | 75 + netwerk/test/unit/test_http_sfv.js | 590 ++++++ netwerk/test/unit/test_httpauth.js | 204 ++ netwerk/test/unit/test_httpcancel.js | 260 +++ netwerk/test/unit/test_httpssvc_https_upgrade.js | 298 +++ netwerk/test/unit/test_httpssvc_iphint.js | 301 +++ netwerk/test/unit/test_httpssvc_priority.js | 235 +++ netwerk/test/unit/test_httpssvc_retry_with_ech.js | 288 +++ .../test/unit/test_httpssvc_retry_without_ech.js | 223 ++ netwerk/test/unit/test_httpsuspend.js | 84 + netwerk/test/unit/test_idn_blacklist.js | 173 ++ netwerk/test/unit/test_idn_urls.js | 441 ++++ netwerk/test/unit/test_idna2008.js | 65 + netwerk/test/unit/test_idnservice.js | 39 + netwerk/test/unit/test_immutable.js | 167 ++ netwerk/test/unit/test_inhibit_caching.js | 92 + netwerk/test/unit/test_ioservice.js | 19 + netwerk/test/unit/test_large_port.js | 68 + netwerk/test/unit/test_link.desktop | 3 + netwerk/test/unit/test_link.lnk | Bin 0 -> 345 bytes netwerk/test/unit/test_link.url | 5 + netwerk/test/unit/test_loadgroup_cancel.js | 94 + netwerk/test/unit/test_localstreams.js | 90 + netwerk/test/unit/test_mismatch_last-modified.js | 155 ++ netwerk/test/unit/test_mozTXTToHTMLConv.js | 395 ++++ netwerk/test/unit/test_multipart_byteranges.js | 141 ++ .../unit/test_multipart_streamconv-byte-by-byte.js | 113 ++ netwerk/test/unit/test_multipart_streamconv.js | 98 + .../test/unit/test_multipart_streamconv_empty.js | 68 + ...part_streamconv_missing_boundary_lead_dashes.js | 90 + ...t_multipart_streamconv_missing_lead_boundary.js | 90 + netwerk/test/unit/test_nestedabout_serialize.js | 41 + netwerk/test/unit/test_net_addr.js | 222 ++ netwerk/test/unit/test_network_activity.js | 61 + .../test/unit/test_network_connectivity_service.js | 215 ++ .../unit/test_no_cookies_after_last_pb_exit.js | 134 ++ netwerk/test/unit/test_node_execute.js | 87 + netwerk/test/unit/test_nojsredir.js | 65 + ...test_nsIBufferedOutputStream_writeFrom_block.js | 193 ++ netwerk/test/unit/test_obs-fold.js | 73 + netwerk/test/unit/test_offline_status.js | 19 + .../unit/test_offlinecache_custom-directory.js | 165 ++ netwerk/test/unit/test_origin.js | 330 +++ .../test/unit/test_original_sent_received_head.js | 246 +++ netwerk/test/unit/test_parse_content_type.js | 365 ++++ ...est_partial_response_entry_size_smart_shrink.js | 104 + netwerk/test/unit/test_permmgr.js | 131 ++ netwerk/test/unit/test_ping_aboutnetworking.js | 105 + netwerk/test/unit/test_pinned_app_cache.js | 302 +++ netwerk/test/unit/test_plaintext_sniff.js | 209 ++ netwerk/test/unit/test_port_remapping.js | 48 + netwerk/test/unit/test_post.js | 139 ++ netwerk/test/unit/test_predictor.js | 771 +++++++ netwerk/test/unit/test_private_cookie_changed.js | 44 + netwerk/test/unit/test_private_necko_channel.js | 56 + netwerk/test/unit/test_progress.js | 132 ++ .../test_protocolproxyservice-async-filters.js | 440 ++++ netwerk/test/unit/test_protocolproxyservice.js | 1049 ++++++++++ netwerk/test/unit/test_proxy-failover_canceled.js | 58 + netwerk/test/unit/test_proxy-failover_passing.js | 46 + netwerk/test/unit/test_proxy-replace_canceled.js | 58 + netwerk/test/unit/test_proxy-replace_passing.js | 46 + netwerk/test/unit/test_proxyconnect.js | 358 ++++ netwerk/test/unit/test_psl.js | 45 + netwerk/test/unit/test_race_cache_with_network.js | 284 +++ netwerk/test/unit/test_range_requests.js | 527 +++++ .../unit/test_rcwn_always_cache_new_content.js | 115 ++ netwerk/test/unit/test_readline.js | 104 + .../test/unit/test_redirect-caching_canceled.js | 62 + netwerk/test/unit/test_redirect-caching_failure.js | 82 + netwerk/test/unit/test_redirect-caching_passing.js | 54 + netwerk/test/unit/test_redirect_baduri.js | 42 + netwerk/test/unit/test_redirect_canceled.js | 48 + .../test/unit/test_redirect_different-protocol.js | 54 + netwerk/test/unit/test_redirect_failure.js | 60 + netwerk/test/unit/test_redirect_from_script.js | 249 +++ ...test_redirect_from_script_after-open_passing.js | 249 +++ netwerk/test/unit/test_redirect_history.js | 74 + netwerk/test/unit/test_redirect_loop.js | 86 + netwerk/test/unit/test_redirect_passing.js | 53 + .../test/unit/test_redirect_protocol_telemetry.js | 66 + netwerk/test/unit/test_reentrancy.js | 107 + netwerk/test/unit/test_referrer.js | 247 +++ netwerk/test/unit/test_referrer_cross_origin.js | 319 +++ netwerk/test/unit/test_referrer_policy.js | 143 ++ netwerk/test/unit/test_reopen.js | 147 ++ .../test/unit/test_reply_without_content_type.js | 150 ++ netwerk/test/unit/test_resumable_channel.js | 424 ++++ netwerk/test/unit/test_resumable_truncate.js | 93 + netwerk/test/unit/test_safeoutputstream.js | 72 + netwerk/test/unit/test_safeoutputstream_append.js | 45 + netwerk/test/unit/test_schema_10_migration.js | 181 ++ netwerk/test/unit/test_schema_2_migration.js | 303 +++ netwerk/test/unit/test_schema_3_migration.js | 170 ++ netwerk/test/unit/test_separate_connections.js | 102 + netwerk/test/unit/test_signature_extraction.js | 215 ++ netwerk/test/unit/test_simple.js | 69 + .../test/unit/test_sockettransportsvc_available.js | 11 + netwerk/test/unit/test_socks.js | 519 +++++ netwerk/test/unit/test_speculative_connect.js | 368 ++++ .../test/unit/test_stale-while-revalidate_loop.js | 46 + .../unit/test_stale-while-revalidate_max-age-0.js | 111 + .../unit/test_stale-while-revalidate_negative.js | 90 + .../unit/test_stale-while-revalidate_positive.js | 111 + netwerk/test/unit/test_standardurl.js | 1299 ++++++++++++ netwerk/test/unit/test_standardurl_default_port.js | 61 + netwerk/test/unit/test_standardurl_port.js | 71 + netwerk/test/unit/test_streamcopier.js | 63 + .../unit/test_substituting_protocol_handler.js | 64 + .../unit/test_suspend_channel_before_connect.js | 99 + .../test/unit/test_suspend_channel_on_authRetry.js | 276 +++ .../test/unit/test_suspend_channel_on_examine.js | 77 + ...t_suspend_channel_on_examine_merged_response.js | 212 ++ .../test/unit/test_suspend_channel_on_modified.js | 181 ++ netwerk/test/unit/test_synthesized_response.js | 294 +++ netwerk/test/unit/test_throttlechannel.js | 46 + netwerk/test/unit/test_throttlequeue.js | 25 + netwerk/test/unit/test_throttling.js | 64 + netwerk/test/unit/test_tldservice_nextsubdomain.js | 28 + netwerk/test/unit/test_tls_flags.js | 274 +++ .../unit/test_tls_flags_separate_connections.js | 119 ++ netwerk/test/unit/test_tls_server.js | 301 +++ .../test/unit/test_tls_server_multiple_clients.js | 152 ++ netwerk/test/unit/test_traceable_channel.js | 145 ++ .../test_trackingProtection_annotateChannels.js | 388 ++++ netwerk/test/unit/test_trr.js | 2136 ++++++++++++++++++++ netwerk/test/unit/test_trr_additional_section.js | 314 +++ netwerk/test/unit/test_trr_case_sensitivity.js | 143 ++ netwerk/test/unit/test_trr_cname_chain.js | 110 + netwerk/test/unit/test_trr_extended_error.js | 340 ++++ netwerk/test/unit/test_trr_https_fallback.js | 1090 ++++++++++ netwerk/test/unit/test_trr_httpssvc.js | 669 ++++++ netwerk/test/unit/test_trr_nat64.js | 121 ++ netwerk/test/unit/test_trr_proxy.js | 133 ++ netwerk/test/unit/test_udp_multicast.js | 114 ++ netwerk/test/unit/test_udpsocket.js | 89 + netwerk/test/unit/test_udpsocket_offline.js | 108 + netwerk/test/unit/test_unescapestring.js | 34 + netwerk/test/unit/test_unix_domain.js | 705 +++++++ netwerk/test/unit/test_uri_mutator.js | 48 + netwerk/test/unit/test_use_httpssvc.js | 325 +++ netwerk/test/unit/test_websocket_offline.js | 51 + netwerk/test/unit/test_xmlhttprequest.js | 55 + netwerk/test/unit/xpcshell.ini | 517 +++++ 428 files changed, 69303 insertions(+) create mode 100644 netwerk/test/unit/CA.key.pem create mode 100644 netwerk/test/unit/client_cert_chooser.js create mode 100644 netwerk/test/unit/client_cert_chooser.manifest create mode 100644 netwerk/test/unit/data/cookies_v10.sqlite create mode 100644 netwerk/test/unit/data/image.png create mode 100644 netwerk/test/unit/data/signed_win.exe create mode 100644 netwerk/test/unit/data/system_root.lnk create mode 100644 netwerk/test/unit/data/test_psl.txt create mode 100644 netwerk/test/unit/data/test_readline1.txt create mode 100644 netwerk/test/unit/data/test_readline2.txt create mode 100644 netwerk/test/unit/data/test_readline3.txt create mode 100644 netwerk/test/unit/data/test_readline4.txt create mode 100644 netwerk/test/unit/data/test_readline5.txt create mode 100644 netwerk/test/unit/data/test_readline6.txt create mode 100644 netwerk/test/unit/data/test_readline7.txt create mode 100644 netwerk/test/unit/data/test_readline8.txt create mode 100644 netwerk/test/unit/head_cache.js create mode 100644 netwerk/test/unit/head_cache2.js create mode 100644 netwerk/test/unit/head_channels.js create mode 100644 netwerk/test/unit/head_cookies.js create mode 100644 netwerk/test/unit/head_http3.js create mode 100644 netwerk/test/unit/head_trr.js create mode 100644 netwerk/test/unit/http2-ca.pem create mode 100644 netwerk/test/unit/http2-ca.pem.certspec create mode 100644 netwerk/test/unit/moz.build create mode 100644 netwerk/test/unit/perftest.ini create mode 100644 netwerk/test/unit/socks_client_subprocess.js create mode 100644 netwerk/test/unit/test_1073747.js create mode 100644 netwerk/test/unit/test_304_responses.js create mode 100644 netwerk/test/unit/test_307_redirect.js create mode 100644 netwerk/test/unit/test_421.js create mode 100644 netwerk/test/unit/test_MIME_params.js create mode 100644 netwerk/test/unit/test_NetUtil.js create mode 100644 netwerk/test/unit/test_SuperfluousAuth.js create mode 100644 netwerk/test/unit/test_URIs.js create mode 100644 netwerk/test/unit/test_URIs2.js create mode 100644 netwerk/test/unit/test_XHR_redirects.js create mode 100644 netwerk/test/unit/test_about_networking.js create mode 100644 netwerk/test/unit/test_about_protocol.js create mode 100644 netwerk/test/unit/test_aboutblank.js create mode 100644 netwerk/test/unit/test_addr_in_use_error.js create mode 100644 netwerk/test/unit/test_alt-data_closeWithStatus.js create mode 100644 netwerk/test/unit/test_alt-data_cross_process.js create mode 100644 netwerk/test/unit/test_alt-data_overwrite.js create mode 100644 netwerk/test/unit/test_alt-data_simple.js create mode 100644 netwerk/test/unit/test_alt-data_stream.js create mode 100644 netwerk/test/unit/test_alt-data_too_big.js create mode 100644 netwerk/test/unit/test_altsvc.js create mode 100644 netwerk/test/unit/test_altsvc_http3.js create mode 100644 netwerk/test/unit/test_altsvc_pref.js create mode 100644 netwerk/test/unit/test_anonymous-coalescing.js create mode 100644 netwerk/test/unit/test_auth_dialog_permission.js create mode 100644 netwerk/test/unit/test_auth_jar.js create mode 100644 netwerk/test/unit/test_auth_proxy.js create mode 100644 netwerk/test/unit/test_authentication.js create mode 100644 netwerk/test/unit/test_authpromptwrapper.js create mode 100644 netwerk/test/unit/test_backgroundfilesaver.js create mode 100644 netwerk/test/unit/test_be_conservative.js create mode 100644 netwerk/test/unit/test_be_conservative_error_handling.js create mode 100644 netwerk/test/unit/test_blob_channelname.js create mode 100644 netwerk/test/unit/test_brotli_http.js create mode 100644 netwerk/test/unit/test_bug1064258.js create mode 100644 netwerk/test/unit/test_bug1177909.js create mode 100644 netwerk/test/unit/test_bug1195415.js create mode 100644 netwerk/test/unit/test_bug1218029.js create mode 100644 netwerk/test/unit/test_bug1279246.js create mode 100644 netwerk/test/unit/test_bug1312774_http1.js create mode 100644 netwerk/test/unit/test_bug1312782_http1.js create mode 100644 netwerk/test/unit/test_bug1355539_http1.js create mode 100644 netwerk/test/unit/test_bug1378385_http1.js create mode 100644 netwerk/test/unit/test_bug1411316_http1.js create mode 100644 netwerk/test/unit/test_bug1527293.js create mode 100644 netwerk/test/unit/test_bug1683176.js create mode 100644 netwerk/test/unit/test_bug203271.js create mode 100644 netwerk/test/unit/test_bug248970_cache.js create mode 100644 netwerk/test/unit/test_bug248970_cookie.js create mode 100644 netwerk/test/unit/test_bug261425.js create mode 100644 netwerk/test/unit/test_bug263127.js create mode 100644 netwerk/test/unit/test_bug282432.js create mode 100644 netwerk/test/unit/test_bug321706.js create mode 100644 netwerk/test/unit/test_bug331825.js create mode 100644 netwerk/test/unit/test_bug336501.js create mode 100644 netwerk/test/unit/test_bug337744.js create mode 100644 netwerk/test/unit/test_bug365133.js create mode 100644 netwerk/test/unit/test_bug368702.js create mode 100644 netwerk/test/unit/test_bug369787.js create mode 100644 netwerk/test/unit/test_bug371473.js create mode 100644 netwerk/test/unit/test_bug376844.js create mode 100644 netwerk/test/unit/test_bug376865.js create mode 100644 netwerk/test/unit/test_bug379034.js create mode 100644 netwerk/test/unit/test_bug380994.js create mode 100644 netwerk/test/unit/test_bug388281.js create mode 100644 netwerk/test/unit/test_bug396389.js create mode 100644 netwerk/test/unit/test_bug401564.js create mode 100644 netwerk/test/unit/test_bug411952.js create mode 100644 netwerk/test/unit/test_bug412457.js create mode 100644 netwerk/test/unit/test_bug412945.js create mode 100644 netwerk/test/unit/test_bug414122.js create mode 100644 netwerk/test/unit/test_bug427957.js create mode 100644 netwerk/test/unit/test_bug429347.js create mode 100644 netwerk/test/unit/test_bug455311.js create mode 100644 netwerk/test/unit/test_bug464591.js create mode 100644 netwerk/test/unit/test_bug468426.js create mode 100644 netwerk/test/unit/test_bug468594.js create mode 100644 netwerk/test/unit/test_bug470716.js create mode 100644 netwerk/test/unit/test_bug477578.js create mode 100644 netwerk/test/unit/test_bug479413.js create mode 100644 netwerk/test/unit/test_bug479485.js create mode 100644 netwerk/test/unit/test_bug482601.js create mode 100644 netwerk/test/unit/test_bug482934.js create mode 100644 netwerk/test/unit/test_bug484684.js create mode 100644 netwerk/test/unit/test_bug490095.js create mode 100644 netwerk/test/unit/test_bug504014.js create mode 100644 netwerk/test/unit/test_bug510359.js create mode 100644 netwerk/test/unit/test_bug515583.js create mode 100644 netwerk/test/unit/test_bug526789.js create mode 100644 netwerk/test/unit/test_bug528292.js create mode 100644 netwerk/test/unit/test_bug536324_64bit_content_length.js create mode 100644 netwerk/test/unit/test_bug540566.js create mode 100644 netwerk/test/unit/test_bug543805.js create mode 100644 netwerk/test/unit/test_bug553970.js create mode 100644 netwerk/test/unit/test_bug561042.js create mode 100644 netwerk/test/unit/test_bug561276.js create mode 100644 netwerk/test/unit/test_bug580508.js create mode 100644 netwerk/test/unit/test_bug586908.js create mode 100644 netwerk/test/unit/test_bug596443.js create mode 100644 netwerk/test/unit/test_bug618835.js create mode 100644 netwerk/test/unit/test_bug633743.js create mode 100644 netwerk/test/unit/test_bug650522.js create mode 100644 netwerk/test/unit/test_bug650995.js create mode 100644 netwerk/test/unit/test_bug652761.js create mode 100644 netwerk/test/unit/test_bug654926.js create mode 100644 netwerk/test/unit/test_bug654926_doom_and_read.js create mode 100644 netwerk/test/unit/test_bug654926_test_seek.js create mode 100644 netwerk/test/unit/test_bug659569.js create mode 100644 netwerk/test/unit/test_bug660066.js create mode 100644 netwerk/test/unit/test_bug667087.js create mode 100644 netwerk/test/unit/test_bug667818.js create mode 100644 netwerk/test/unit/test_bug667907.js create mode 100644 netwerk/test/unit/test_bug669001.js create mode 100644 netwerk/test/unit/test_bug767025.js create mode 100644 netwerk/test/unit/test_bug770243.js create mode 100644 netwerk/test/unit/test_bug812167.js create mode 100644 netwerk/test/unit/test_bug826063.js create mode 100644 netwerk/test/unit/test_bug856978.js create mode 100644 netwerk/test/unit/test_bug894586.js create mode 100644 netwerk/test/unit/test_bug935499.js create mode 100644 netwerk/test/unit/test_cache-control_request.js create mode 100644 netwerk/test/unit/test_cache-entry-id.js create mode 100644 netwerk/test/unit/test_cache2-00-service-get.js create mode 100644 netwerk/test/unit/test_cache2-01-basic.js create mode 100644 netwerk/test/unit/test_cache2-01a-basic-readonly.js create mode 100644 netwerk/test/unit/test_cache2-01b-basic-datasize.js create mode 100644 netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js create mode 100644 netwerk/test/unit/test_cache2-01d-basic-not-wanted.js create mode 100644 netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js create mode 100644 netwerk/test/unit/test_cache2-01f-basic-openTruncate.js create mode 100644 netwerk/test/unit/test_cache2-02-open-non-existing.js create mode 100644 netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js create mode 100644 netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js create mode 100644 netwerk/test/unit/test_cache2-05-visit.js create mode 100644 netwerk/test/unit/test_cache2-06-pb-mode.js create mode 100644 netwerk/test/unit/test_cache2-07-visit-memory.js create mode 100644 netwerk/test/unit/test_cache2-07a-open-memory.js create mode 100644 netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js create mode 100644 netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js create mode 100644 netwerk/test/unit/test_cache2-10-evict-direct.js create mode 100644 netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js create mode 100644 netwerk/test/unit/test_cache2-11-evict-memory.js create mode 100644 netwerk/test/unit/test_cache2-12-evict-disk.js create mode 100644 netwerk/test/unit/test_cache2-13-evict-non-existing.js create mode 100644 netwerk/test/unit/test_cache2-14-concurent-readers.js create mode 100644 netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js create mode 100644 netwerk/test/unit/test_cache2-15-conditional-304.js create mode 100644 netwerk/test/unit/test_cache2-16-conditional-200.js create mode 100644 netwerk/test/unit/test_cache2-17-evict-all.js create mode 100644 netwerk/test/unit/test_cache2-18-not-valid.js create mode 100644 netwerk/test/unit/test_cache2-19-range-206.js create mode 100644 netwerk/test/unit/test_cache2-20-range-200.js create mode 100644 netwerk/test/unit/test_cache2-21-anon-storage.js create mode 100644 netwerk/test/unit/test_cache2-22-anon-visit.js create mode 100644 netwerk/test/unit/test_cache2-23-read-over-chunk.js create mode 100644 netwerk/test/unit/test_cache2-24-exists.js create mode 100644 netwerk/test/unit/test_cache2-25-chunk-memory-limit.js create mode 100644 netwerk/test/unit/test_cache2-26-no-outputstream-open.js create mode 100644 netwerk/test/unit/test_cache2-27-force-valid-for.js create mode 100644 netwerk/test/unit/test_cache2-28-last-access-attrs.js create mode 100644 netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js create mode 100644 netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js create mode 100644 netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js create mode 100644 netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js create mode 100644 netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js create mode 100644 netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js create mode 100644 netwerk/test/unit/test_cache2-30a-entry-pinning.js create mode 100644 netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js create mode 100644 netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js create mode 100644 netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js create mode 100644 netwerk/test/unit/test_cache2-31-visit-all.js create mode 100644 netwerk/test/unit/test_cache2-32-clear-origin.js create mode 100644 netwerk/test/unit/test_cacheForOfflineUse_no-store.js create mode 100644 netwerk/test/unit/test_cache_204_response.js create mode 100644 netwerk/test/unit/test_cache_jar.js create mode 100644 netwerk/test/unit/test_cacheflags.js create mode 100644 netwerk/test/unit/test_captive_portal_service.js create mode 100644 netwerk/test/unit/test_channel_close.js create mode 100644 netwerk/test/unit/test_channel_priority.js create mode 100644 netwerk/test/unit/test_chunked_responses.js create mode 100644 netwerk/test/unit/test_compareURIs.js create mode 100644 netwerk/test/unit/test_compressappend.js create mode 100644 netwerk/test/unit/test_content_encoding_gzip.js create mode 100644 netwerk/test/unit/test_content_length_underrun.js create mode 100644 netwerk/test/unit/test_content_sniffer.js create mode 100644 netwerk/test/unit/test_cookie_blacklist.js create mode 100644 netwerk/test/unit/test_cookie_header.js create mode 100644 netwerk/test/unit/test_cookie_ipv6.js create mode 100644 netwerk/test/unit/test_cookiejars.js create mode 100644 netwerk/test/unit/test_cookiejars_safebrowsing.js create mode 100644 netwerk/test/unit/test_cookies_async_failure.js create mode 100644 netwerk/test/unit/test_cookies_persistence.js create mode 100644 netwerk/test/unit/test_cookies_privatebrowsing.js create mode 100644 netwerk/test/unit/test_cookies_profile_close.js create mode 100644 netwerk/test/unit/test_cookies_read.js create mode 100644 netwerk/test/unit/test_cookies_sync_failure.js create mode 100644 netwerk/test/unit/test_cookies_thirdparty.js create mode 100644 netwerk/test/unit/test_cookies_thirdparty_session.js create mode 100644 netwerk/test/unit/test_cookies_upgrade_10.js create mode 100644 netwerk/test/unit/test_data_protocol.js create mode 100644 netwerk/test/unit/test_defaultURI.js create mode 100644 netwerk/test/unit/test_disabled_ftp.js create mode 100644 netwerk/test/unit/test_dns_by_type_resolve.js create mode 100644 netwerk/test/unit/test_dns_cancel.js create mode 100644 netwerk/test/unit/test_dns_disable_ipv4.js create mode 100644 netwerk/test/unit/test_dns_disable_ipv6.js create mode 100644 netwerk/test/unit/test_dns_disabled.js create mode 100644 netwerk/test/unit/test_dns_localredirect.js create mode 100644 netwerk/test/unit/test_dns_offline.js create mode 100644 netwerk/test/unit/test_dns_onion.js create mode 100644 netwerk/test/unit/test_dns_originAttributes.js create mode 100644 netwerk/test/unit/test_dns_override.js create mode 100644 netwerk/test/unit/test_dns_override_for_localhost.js create mode 100644 netwerk/test/unit/test_dns_proxy_bypass.js create mode 100644 netwerk/test/unit/test_dns_service.js create mode 100644 netwerk/test/unit/test_domain_eviction.js create mode 100644 netwerk/test/unit/test_doomentry.js create mode 100644 netwerk/test/unit/test_duplicate_headers.js create mode 100644 netwerk/test/unit/test_event_sink.js create mode 100644 netwerk/test/unit/test_eviction.js create mode 100644 netwerk/test/unit/test_extract_charset_from_content_type.js create mode 100644 netwerk/test/unit/test_fallback_no-cache-entry_canceled.js create mode 100644 netwerk/test/unit/test_fallback_no-cache-entry_passing.js create mode 100644 netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js create mode 100644 netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js create mode 100644 netwerk/test/unit/test_fallback_request-error_canceled.js create mode 100644 netwerk/test/unit/test_fallback_request-error_passing.js create mode 100644 netwerk/test/unit/test_fallback_response-error_canceled.js create mode 100644 netwerk/test/unit/test_fallback_response-error_passing.js create mode 100644 netwerk/test/unit/test_file_protocol.js create mode 100644 netwerk/test/unit/test_filestreams.js create mode 100644 netwerk/test/unit/test_freshconnection.js create mode 100644 netwerk/test/unit/test_getHost.js create mode 100644 netwerk/test/unit/test_gre_resources.js create mode 100644 netwerk/test/unit/test_gzipped_206.js create mode 100644 netwerk/test/unit/test_head.js create mode 100644 netwerk/test/unit/test_head_request_no_response_body.js create mode 100644 netwerk/test/unit/test_header_Accept-Language.js create mode 100644 netwerk/test/unit/test_header_Accept-Language_case.js create mode 100644 netwerk/test/unit/test_header_Server_Timing.js create mode 100644 netwerk/test/unit/test_headers.js create mode 100644 netwerk/test/unit/test_hostnameIsLocalIPAddress.js create mode 100644 netwerk/test/unit/test_hostnameIsSharedIPAddress.js create mode 100644 netwerk/test/unit/test_http1-proxy.js create mode 100644 netwerk/test/unit/test_http2-proxy.js create mode 100644 netwerk/test/unit/test_http2.js create mode 100644 netwerk/test/unit/test_http3.js create mode 100644 netwerk/test/unit/test_http3_421.js create mode 100644 netwerk/test/unit/test_http3_alt_svc.js create mode 100644 netwerk/test/unit/test_http3_error_before_connect.js create mode 100644 netwerk/test/unit/test_http3_fast_fallback.js create mode 100644 netwerk/test/unit/test_http3_fatal_stream_error.js create mode 100644 netwerk/test/unit/test_http3_large_post.js create mode 100644 netwerk/test/unit/test_http3_perf.js create mode 100644 netwerk/test/unit/test_http3_server_not_existing.js create mode 100644 netwerk/test/unit/test_http3_trans_close.js create mode 100644 netwerk/test/unit/test_httpResponseTimeout.js create mode 100644 netwerk/test/unit/test_http_headers.js create mode 100644 netwerk/test/unit/test_http_sfv.js create mode 100644 netwerk/test/unit/test_httpauth.js create mode 100644 netwerk/test/unit/test_httpcancel.js create mode 100644 netwerk/test/unit/test_httpssvc_https_upgrade.js create mode 100644 netwerk/test/unit/test_httpssvc_iphint.js create mode 100644 netwerk/test/unit/test_httpssvc_priority.js create mode 100644 netwerk/test/unit/test_httpssvc_retry_with_ech.js create mode 100644 netwerk/test/unit/test_httpssvc_retry_without_ech.js create mode 100644 netwerk/test/unit/test_httpsuspend.js create mode 100644 netwerk/test/unit/test_idn_blacklist.js create mode 100644 netwerk/test/unit/test_idn_urls.js create mode 100644 netwerk/test/unit/test_idna2008.js create mode 100644 netwerk/test/unit/test_idnservice.js create mode 100644 netwerk/test/unit/test_immutable.js create mode 100644 netwerk/test/unit/test_inhibit_caching.js create mode 100644 netwerk/test/unit/test_ioservice.js create mode 100644 netwerk/test/unit/test_large_port.js create mode 100644 netwerk/test/unit/test_link.desktop create mode 100644 netwerk/test/unit/test_link.lnk create mode 100644 netwerk/test/unit/test_link.url create mode 100644 netwerk/test/unit/test_loadgroup_cancel.js create mode 100644 netwerk/test/unit/test_localstreams.js create mode 100644 netwerk/test/unit/test_mismatch_last-modified.js create mode 100644 netwerk/test/unit/test_mozTXTToHTMLConv.js create mode 100644 netwerk/test/unit/test_multipart_byteranges.js create mode 100644 netwerk/test/unit/test_multipart_streamconv-byte-by-byte.js create mode 100644 netwerk/test/unit/test_multipart_streamconv.js create mode 100644 netwerk/test/unit/test_multipart_streamconv_empty.js create mode 100644 netwerk/test/unit/test_multipart_streamconv_missing_boundary_lead_dashes.js create mode 100644 netwerk/test/unit/test_multipart_streamconv_missing_lead_boundary.js create mode 100644 netwerk/test/unit/test_nestedabout_serialize.js create mode 100644 netwerk/test/unit/test_net_addr.js create mode 100644 netwerk/test/unit/test_network_activity.js create mode 100644 netwerk/test/unit/test_network_connectivity_service.js create mode 100644 netwerk/test/unit/test_no_cookies_after_last_pb_exit.js create mode 100644 netwerk/test/unit/test_node_execute.js create mode 100644 netwerk/test/unit/test_nojsredir.js create mode 100644 netwerk/test/unit/test_nsIBufferedOutputStream_writeFrom_block.js create mode 100644 netwerk/test/unit/test_obs-fold.js create mode 100644 netwerk/test/unit/test_offline_status.js create mode 100644 netwerk/test/unit/test_offlinecache_custom-directory.js create mode 100644 netwerk/test/unit/test_origin.js create mode 100644 netwerk/test/unit/test_original_sent_received_head.js create mode 100644 netwerk/test/unit/test_parse_content_type.js create mode 100644 netwerk/test/unit/test_partial_response_entry_size_smart_shrink.js create mode 100644 netwerk/test/unit/test_permmgr.js create mode 100644 netwerk/test/unit/test_ping_aboutnetworking.js create mode 100644 netwerk/test/unit/test_pinned_app_cache.js create mode 100644 netwerk/test/unit/test_plaintext_sniff.js create mode 100644 netwerk/test/unit/test_port_remapping.js create mode 100644 netwerk/test/unit/test_post.js create mode 100644 netwerk/test/unit/test_predictor.js create mode 100644 netwerk/test/unit/test_private_cookie_changed.js create mode 100644 netwerk/test/unit/test_private_necko_channel.js create mode 100644 netwerk/test/unit/test_progress.js create mode 100644 netwerk/test/unit/test_protocolproxyservice-async-filters.js create mode 100644 netwerk/test/unit/test_protocolproxyservice.js create mode 100644 netwerk/test/unit/test_proxy-failover_canceled.js create mode 100644 netwerk/test/unit/test_proxy-failover_passing.js create mode 100644 netwerk/test/unit/test_proxy-replace_canceled.js create mode 100644 netwerk/test/unit/test_proxy-replace_passing.js create mode 100644 netwerk/test/unit/test_proxyconnect.js create mode 100644 netwerk/test/unit/test_psl.js create mode 100644 netwerk/test/unit/test_race_cache_with_network.js create mode 100644 netwerk/test/unit/test_range_requests.js create mode 100644 netwerk/test/unit/test_rcwn_always_cache_new_content.js create mode 100644 netwerk/test/unit/test_readline.js create mode 100644 netwerk/test/unit/test_redirect-caching_canceled.js create mode 100644 netwerk/test/unit/test_redirect-caching_failure.js create mode 100644 netwerk/test/unit/test_redirect-caching_passing.js create mode 100644 netwerk/test/unit/test_redirect_baduri.js create mode 100644 netwerk/test/unit/test_redirect_canceled.js create mode 100644 netwerk/test/unit/test_redirect_different-protocol.js create mode 100644 netwerk/test/unit/test_redirect_failure.js create mode 100644 netwerk/test/unit/test_redirect_from_script.js create mode 100644 netwerk/test/unit/test_redirect_from_script_after-open_passing.js create mode 100644 netwerk/test/unit/test_redirect_history.js create mode 100644 netwerk/test/unit/test_redirect_loop.js create mode 100644 netwerk/test/unit/test_redirect_passing.js create mode 100644 netwerk/test/unit/test_redirect_protocol_telemetry.js create mode 100644 netwerk/test/unit/test_reentrancy.js create mode 100644 netwerk/test/unit/test_referrer.js create mode 100644 netwerk/test/unit/test_referrer_cross_origin.js create mode 100644 netwerk/test/unit/test_referrer_policy.js create mode 100644 netwerk/test/unit/test_reopen.js create mode 100644 netwerk/test/unit/test_reply_without_content_type.js create mode 100644 netwerk/test/unit/test_resumable_channel.js create mode 100644 netwerk/test/unit/test_resumable_truncate.js create mode 100644 netwerk/test/unit/test_safeoutputstream.js create mode 100644 netwerk/test/unit/test_safeoutputstream_append.js create mode 100644 netwerk/test/unit/test_schema_10_migration.js create mode 100644 netwerk/test/unit/test_schema_2_migration.js create mode 100644 netwerk/test/unit/test_schema_3_migration.js create mode 100644 netwerk/test/unit/test_separate_connections.js create mode 100644 netwerk/test/unit/test_signature_extraction.js create mode 100644 netwerk/test/unit/test_simple.js create mode 100644 netwerk/test/unit/test_sockettransportsvc_available.js create mode 100644 netwerk/test/unit/test_socks.js create mode 100644 netwerk/test/unit/test_speculative_connect.js create mode 100644 netwerk/test/unit/test_stale-while-revalidate_loop.js create mode 100644 netwerk/test/unit/test_stale-while-revalidate_max-age-0.js create mode 100644 netwerk/test/unit/test_stale-while-revalidate_negative.js create mode 100644 netwerk/test/unit/test_stale-while-revalidate_positive.js create mode 100644 netwerk/test/unit/test_standardurl.js create mode 100644 netwerk/test/unit/test_standardurl_default_port.js create mode 100644 netwerk/test/unit/test_standardurl_port.js create mode 100644 netwerk/test/unit/test_streamcopier.js create mode 100644 netwerk/test/unit/test_substituting_protocol_handler.js create mode 100644 netwerk/test/unit/test_suspend_channel_before_connect.js create mode 100644 netwerk/test/unit/test_suspend_channel_on_authRetry.js create mode 100644 netwerk/test/unit/test_suspend_channel_on_examine.js create mode 100644 netwerk/test/unit/test_suspend_channel_on_examine_merged_response.js create mode 100644 netwerk/test/unit/test_suspend_channel_on_modified.js create mode 100644 netwerk/test/unit/test_synthesized_response.js create mode 100644 netwerk/test/unit/test_throttlechannel.js create mode 100644 netwerk/test/unit/test_throttlequeue.js create mode 100644 netwerk/test/unit/test_throttling.js create mode 100644 netwerk/test/unit/test_tldservice_nextsubdomain.js create mode 100644 netwerk/test/unit/test_tls_flags.js create mode 100644 netwerk/test/unit/test_tls_flags_separate_connections.js create mode 100644 netwerk/test/unit/test_tls_server.js create mode 100644 netwerk/test/unit/test_tls_server_multiple_clients.js create mode 100644 netwerk/test/unit/test_traceable_channel.js create mode 100644 netwerk/test/unit/test_trackingProtection_annotateChannels.js create mode 100644 netwerk/test/unit/test_trr.js create mode 100644 netwerk/test/unit/test_trr_additional_section.js create mode 100644 netwerk/test/unit/test_trr_case_sensitivity.js create mode 100644 netwerk/test/unit/test_trr_cname_chain.js create mode 100644 netwerk/test/unit/test_trr_extended_error.js create mode 100644 netwerk/test/unit/test_trr_https_fallback.js create mode 100644 netwerk/test/unit/test_trr_httpssvc.js create mode 100644 netwerk/test/unit/test_trr_nat64.js create mode 100644 netwerk/test/unit/test_trr_proxy.js create mode 100644 netwerk/test/unit/test_udp_multicast.js create mode 100644 netwerk/test/unit/test_udpsocket.js create mode 100644 netwerk/test/unit/test_udpsocket_offline.js create mode 100644 netwerk/test/unit/test_unescapestring.js create mode 100644 netwerk/test/unit/test_unix_domain.js create mode 100644 netwerk/test/unit/test_uri_mutator.js create mode 100644 netwerk/test/unit/test_use_httpssvc.js create mode 100644 netwerk/test/unit/test_websocket_offline.js create mode 100644 netwerk/test/unit/test_xmlhttprequest.js create mode 100644 netwerk/test/unit/xpcshell.ini (limited to 'netwerk/test/unit') diff --git a/netwerk/test/unit/CA.key.pem b/netwerk/test/unit/CA.key.pem new file mode 100644 index 0000000000..2153c9dd56 --- /dev/null +++ b/netwerk/test/unit/CA.key.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIoXjtIGzP1OkCAggA +MBQGCCqGSIb3DQMHBAg7NkhJaDJEVwSCBMhYUI4JRAIdvJtCmP7lKl30QR+HG4JC +9gUJflQrCsa1WhxKCHa7VXzqlItQTu0FTMwn9GRFUOtqpY8ZE6XJFuvzKPox3RMa +1TMod6v3NS3KIs6/l2Pb2HcGtcMzOJVei1nwtjsT5fvq36x3eoKzgqd9l0fLcvlD +wf+byzgY0Nsau+ER8DWy65jXF8bPsQQcKTc+U+p4moO3UuXG4+Pnd8ooSaM4X2on +1jIYDU1aFoSDUvze8+MvQCD32QLuO63iK7ox4sFharG7KucYqeWCihDx5rlGaVGB +5647v4oHRysEdLVTkU12mIC/Hx/yPXcLhHYmawmnYwEoh1S+wd7rOo9Wn/l16NTK +8BcDuvfM8km4T5oO/UFaNDIBLBQsNM5sNHDYFDlhmR4x6d5nXeERJ6DQbvhQtgnV +bTtT9h24rsC8Irflz/abcvTvqqp8I1+gYEzmhgDRUgp9zAPZUoH3E4DKk5rVgApR +ARX9Y88S7k/OBnU8r+cT+0CjsusbbIv5W2nAFqEX9jMend0cHzYvq3m6v1Jqxjfn +kQRP1n+SagmAPBIAzy1wSHGV43+COk6FB+blfAGbO55lLglEM9PLH7Nnl0XrPtaE +dXx5RTtdBnb349Ow8H3WnleTfKspUbIVNyM48aPaXJu6Y784pUXDOC13ISFVbOew +dPr/s/GoHgBUIm9gxkhNQYUlcSNrJCyJ6bqvrYbOmVQRusO/SaM6ozY8wFL8LDnS +GeXmg3dAslHhuaHlFN7atF7rBtTWPsH+oQdHNKcLDK7nYq45v8VfjPUrWPfYc2nB +l+zT4LozY3VPfPW7BG2zVBTyxXkiynz0w7tJaN/HokZGAUDqWXqjSceJqc9Q4XAG +slIxbxkfxEJUEmJ2wHEnure6T0dJOIfbJzkCqWAeJjkrbI5mdKLuXFj94VgSlfK2 +iq3J20/5HVdHqoVGRZ5rxBUIaVEgSXB3/+9C/M0U0uxx23zxRmVkMGdhhCqXQRh/ +jFUkBzq4x3yibxJW3fRe7jXEJdo1DAAfgBnDvCUWH7lRX8hDkx6OIX4ZS4D7Va0j +ogSC04IdZWxOP3YJ4gGwx8vvgHWnBLyFfmdFnfHXUr9A8HDDJQTupYg25PDUGHla +SxukgOYdQ2O6jUCW0TYeUzX7y/P/Za93kWJp7XqA4v76fQ+C9d3CZT/TY0CqNgxB +C5+PWRGvxtcy+Bne8QYCJhvNPEhfgFa9fU3Rd4w43lvTb9rsy7eBR3jJKdPLKExJ +zEPIgVUGaMf0lawL0fIgoUI5Q3kRCmrswkTK9kr4+rSA//p0NralnZtHCWRvgs9W +Lg4hkf1vXxsa9f16Nk6PxqU/OnJmhTnTnv9MzFoX3Sce2neD86H5c7tdguySbrsj +5fww64rH1UwHhn/i49i3hkseax48gOAZPA8rl+L70FS8dXLpHOm4ihmv6ubVjr82 +yOxi4WmaoXfmOPBgOgGhz1nTFAaetwfhZIsgEtysuWAOsApOUyjlD/wM25988bAa +m5FwslUGLWQfBIV1N9PC+Q0ui1ywRuLoKHNiKDSE+T5iOuv2Yf7du4nncoM/ANmU +FnWJL3Aj1VE/O+OeUyuNEPWLHvVX5TChe5mFXZO4bXfTR4tgdJJ15HWf4LKMQdcl +BEA= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/netwerk/test/unit/client_cert_chooser.js b/netwerk/test/unit/client_cert_chooser.js new file mode 100644 index 0000000000..5078c68fc6 --- /dev/null +++ b/netwerk/test/unit/client_cert_chooser.js @@ -0,0 +1,27 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"use strict"; + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +var Prompter = { + QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]), + alert() {}, // Do nothing when asked to show an alert +}; + +function WindowWatcherService() {} +WindowWatcherService.prototype = { + classID: Components.ID("{01ae923c-81bb-45db-b860-d423b0fc4fe1}"), + QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]), + + getNewPrompter() { + return Prompter; + }, +}; + +this.NSGetFactory = ComponentUtils.generateNSGetFactory([WindowWatcherService]); diff --git a/netwerk/test/unit/client_cert_chooser.manifest b/netwerk/test/unit/client_cert_chooser.manifest new file mode 100644 index 0000000000..e604c92d0e --- /dev/null +++ b/netwerk/test/unit/client_cert_chooser.manifest @@ -0,0 +1,2 @@ +component {01ae923c-81bb-45db-b860-d423b0fc4fe1} cert_dialog.js +contract @mozilla.org/embedcomp/window-watcher;1 {01ae923c-81bb-45db-b860-d423b0fc4fe1} diff --git a/netwerk/test/unit/data/cookies_v10.sqlite b/netwerk/test/unit/data/cookies_v10.sqlite new file mode 100644 index 0000000000..2301731f8e Binary files /dev/null and b/netwerk/test/unit/data/cookies_v10.sqlite differ diff --git a/netwerk/test/unit/data/image.png b/netwerk/test/unit/data/image.png new file mode 100644 index 0000000000..e0c5d3d6a1 Binary files /dev/null and b/netwerk/test/unit/data/image.png differ diff --git a/netwerk/test/unit/data/signed_win.exe b/netwerk/test/unit/data/signed_win.exe new file mode 100644 index 0000000000..de3bb40e84 Binary files /dev/null and b/netwerk/test/unit/data/signed_win.exe differ diff --git a/netwerk/test/unit/data/system_root.lnk b/netwerk/test/unit/data/system_root.lnk new file mode 100644 index 0000000000..e5885ce9a5 Binary files /dev/null and b/netwerk/test/unit/data/system_root.lnk differ diff --git a/netwerk/test/unit/data/test_psl.txt b/netwerk/test/unit/data/test_psl.txt new file mode 100644 index 0000000000..fa6e0d4cec --- /dev/null +++ b/netwerk/test/unit/data/test_psl.txt @@ -0,0 +1,98 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// null input. +checkPublicSuffix(null, null); +// Mixed case. +checkPublicSuffix('COM', null); +checkPublicSuffix('example.COM', 'example.com'); +checkPublicSuffix('WwW.example.COM', 'example.com'); +// Leading dot. +checkPublicSuffix('.com', null); +checkPublicSuffix('.example', null); +checkPublicSuffix('.example.com', null); +checkPublicSuffix('.example.example', null); +// Unlisted TLD. +checkPublicSuffix('example', null); +checkPublicSuffix('example.example', 'example.example'); +checkPublicSuffix('b.example.example', 'example.example'); +checkPublicSuffix('a.b.example.example', 'example.example'); +// Listed, but non-Internet, TLD. +//checkPublicSuffix('local', null); +//checkPublicSuffix('example.local', null); +//checkPublicSuffix('b.example.local', null); +//checkPublicSuffix('a.b.example.local', null); +// TLD with only 1 rule. +checkPublicSuffix('biz', null); +checkPublicSuffix('domain.biz', 'domain.biz'); +checkPublicSuffix('b.domain.biz', 'domain.biz'); +checkPublicSuffix('a.b.domain.biz', 'domain.biz'); +// TLD with some 2-level rules. +checkPublicSuffix('com', null); +checkPublicSuffix('example.com', 'example.com'); +checkPublicSuffix('b.example.com', 'example.com'); +checkPublicSuffix('a.b.example.com', 'example.com'); +checkPublicSuffix('uk.com', null); +checkPublicSuffix('example.uk.com', 'example.uk.com'); +checkPublicSuffix('b.example.uk.com', 'example.uk.com'); +checkPublicSuffix('a.b.example.uk.com', 'example.uk.com'); +checkPublicSuffix('test.ac', 'test.ac'); +// TLD with only 1 (wildcard) rule. +checkPublicSuffix('bd', null); +checkPublicSuffix('c.bd', null); +checkPublicSuffix('b.c.bd', 'b.c.bd'); +checkPublicSuffix('a.b.c.bd', 'b.c.bd'); +// More complex TLD. +checkPublicSuffix('jp', null); +checkPublicSuffix('test.jp', 'test.jp'); +checkPublicSuffix('www.test.jp', 'test.jp'); +checkPublicSuffix('ac.jp', null); +checkPublicSuffix('test.ac.jp', 'test.ac.jp'); +checkPublicSuffix('www.test.ac.jp', 'test.ac.jp'); +checkPublicSuffix('kyoto.jp', null); +checkPublicSuffix('test.kyoto.jp', 'test.kyoto.jp'); +checkPublicSuffix('ide.kyoto.jp', null); +checkPublicSuffix('b.ide.kyoto.jp', 'b.ide.kyoto.jp'); +checkPublicSuffix('a.b.ide.kyoto.jp', 'b.ide.kyoto.jp'); +checkPublicSuffix('c.kobe.jp', null); +checkPublicSuffix('b.c.kobe.jp', 'b.c.kobe.jp'); +checkPublicSuffix('a.b.c.kobe.jp', 'b.c.kobe.jp'); +checkPublicSuffix('city.kobe.jp', 'city.kobe.jp'); +checkPublicSuffix('www.city.kobe.jp', 'city.kobe.jp'); +// TLD with a wildcard rule and exceptions. +checkPublicSuffix('ck', null); +checkPublicSuffix('test.ck', null); +checkPublicSuffix('b.test.ck', 'b.test.ck'); +checkPublicSuffix('a.b.test.ck', 'b.test.ck'); +checkPublicSuffix('www.ck', 'www.ck'); +checkPublicSuffix('www.www.ck', 'www.ck'); +// US K12. +checkPublicSuffix('us', null); +checkPublicSuffix('test.us', 'test.us'); +checkPublicSuffix('www.test.us', 'test.us'); +checkPublicSuffix('ak.us', null); +checkPublicSuffix('test.ak.us', 'test.ak.us'); +checkPublicSuffix('www.test.ak.us', 'test.ak.us'); +checkPublicSuffix('k12.ak.us', null); +checkPublicSuffix('test.k12.ak.us', 'test.k12.ak.us'); +checkPublicSuffix('www.test.k12.ak.us', 'test.k12.ak.us'); +// IDN labels. +checkPublicSuffix('食狮.com.cn', '食狮.com.cn'); +checkPublicSuffix('食狮.公司.cn', '食狮.公司.cn'); +checkPublicSuffix('www.食狮.公司.cn', '食狮.公司.cn'); +checkPublicSuffix('shishi.公司.cn', 'shishi.公司.cn'); +checkPublicSuffix('公司.cn', null); +checkPublicSuffix('食狮.中国', '食狮.中国'); +checkPublicSuffix('www.食狮.中国', '食狮.中国'); +checkPublicSuffix('shishi.中国', 'shishi.中国'); +checkPublicSuffix('中国', null); +// Same as above, but punycoded. +checkPublicSuffix('xn--85x722f.com.cn', 'xn--85x722f.com.cn'); +checkPublicSuffix('xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn'); +checkPublicSuffix('www.xn--85x722f.xn--55qx5d.cn', 'xn--85x722f.xn--55qx5d.cn'); +checkPublicSuffix('shishi.xn--55qx5d.cn', 'shishi.xn--55qx5d.cn'); +checkPublicSuffix('xn--55qx5d.cn', null); +checkPublicSuffix('xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s'); +checkPublicSuffix('www.xn--85x722f.xn--fiqs8s', 'xn--85x722f.xn--fiqs8s'); +checkPublicSuffix('shishi.xn--fiqs8s', 'shishi.xn--fiqs8s'); +checkPublicSuffix('xn--fiqs8s', null); diff --git a/netwerk/test/unit/data/test_readline1.txt b/netwerk/test/unit/data/test_readline1.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/netwerk/test/unit/data/test_readline2.txt b/netwerk/test/unit/data/test_readline2.txt new file mode 100644 index 0000000000..67c3297611 --- /dev/null +++ b/netwerk/test/unit/data/test_readline2.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline3.txt b/netwerk/test/unit/data/test_readline3.txt new file mode 100644 index 0000000000..decdc51878 --- /dev/null +++ b/netwerk/test/unit/data/test_readline3.txt @@ -0,0 +1,3 @@ + + + diff --git a/netwerk/test/unit/data/test_readline4.txt b/netwerk/test/unit/data/test_readline4.txt new file mode 100644 index 0000000000..ca25c36540 --- /dev/null +++ b/netwerk/test/unit/data/test_readline4.txt @@ -0,0 +1,3 @@ +1 + 23 456 +78901 diff --git a/netwerk/test/unit/data/test_readline5.txt b/netwerk/test/unit/data/test_readline5.txt new file mode 100644 index 0000000000..8463b7858e --- /dev/null +++ b/netwerk/test/unit/data/test_readline5.txt @@ -0,0 +1 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE \ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline6.txt b/netwerk/test/unit/data/test_readline6.txt new file mode 100644 index 0000000000..872c40afc4 --- /dev/null +++ b/netwerk/test/unit/data/test_readline6.txt @@ -0,0 +1 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE diff --git a/netwerk/test/unit/data/test_readline7.txt b/netwerk/test/unit/data/test_readline7.txt new file mode 100644 index 0000000000..59ee122ce1 --- /dev/null +++ b/netwerk/test/unit/data/test_readline7.txt @@ -0,0 +1,2 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE + \ No newline at end of file diff --git a/netwerk/test/unit/data/test_readline8.txt b/netwerk/test/unit/data/test_readline8.txt new file mode 100644 index 0000000000..ff6fc09a4a --- /dev/null +++ b/netwerk/test/unit/data/test_readline8.txt @@ -0,0 +1 @@ +zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE \ No newline at end of file diff --git a/netwerk/test/unit/head_cache.js b/netwerk/test/unit/head_cache.js new file mode 100644 index 0000000000..388cb184a2 --- /dev/null +++ b/netwerk/test/unit/head_cache.js @@ -0,0 +1,167 @@ +"use strict"; + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var _CSvc; +function get_cache_service() { + if (_CSvc) { + return _CSvc; + } + + return (_CSvc = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService( + Ci.nsICacheStorageService + )); +} + +function evict_cache_entries(where) { + var clearDisk = !where || where == "disk" || where == "all"; + var clearMem = !where || where == "memory" || where == "all"; + var clearAppCache = where == "appcache"; + + var svc = get_cache_service(); + var storage; + + if (clearMem) { + storage = svc.memoryCacheStorage(Services.loadContextInfo.default); + storage.asyncEvictStorage(null); + } + + if (clearDisk) { + storage = svc.diskCacheStorage(Services.loadContextInfo.default, false); + storage.asyncEvictStorage(null); + } + + if (clearAppCache) { + storage = svc.appCacheStorage(Services.loadContextInfo.default, null); + storage.asyncEvictStorage(null); + } +} + +function createURI(urispec) { + var ioServ = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + return ioServ.newURI(urispec); +} + +function getCacheStorage(where, lci, appcache) { + if (!lci) { + lci = Services.loadContextInfo.default; + } + var svc = get_cache_service(); + switch (where) { + case "disk": + return svc.diskCacheStorage(lci, false); + case "memory": + return svc.memoryCacheStorage(lci); + case "appcache": + return svc.appCacheStorage(lci, appcache); + case "pin": + return svc.pinningCacheStorage(lci); + } + return null; +} + +function asyncOpenCacheEntry(key, where, flags, lci, callback, appcache) { + key = createURI(key); + + function CacheListener() {} + CacheListener.prototype = { + _appCache: appcache, + + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + + onCacheEntryCheck(entry, appCache) { + if (typeof callback === "object") { + return callback.onCacheEntryCheck(entry, appCache); + } + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(entry, isnew, appCache, status) { + if (typeof callback === "object") { + // Root us at the callback + callback.__cache_listener_root = this; + callback.onCacheEntryAvailable(entry, isnew, appCache, status); + } else { + callback(status, entry, appCache); + } + }, + + run() { + var storage = getCacheStorage(where, lci, this._appCache); + storage.asyncOpenURI(key, "", flags, this); + }, + }; + + new CacheListener().run(); +} + +function syncWithCacheIOThread(callback, force) { + if (force) { + asyncOpenCacheEntry( + "http://nonexistententry/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + callback(); + } + ); + } else { + callback(); + } +} + +function get_device_entry_count(where, lci, continuation) { + var storage = getCacheStorage(where, lci); + if (!storage) { + continuation(-1, 0); + return; + } + + var visitor = { + onCacheStorageInfo(entryCount, consumption) { + executeSoon(function() { + continuation(entryCount, consumption); + }); + }, + }; + + // get the device entry count + storage.asyncVisitStorage(visitor, false); +} + +function asyncCheckCacheEntryPresence( + key, + where, + shouldExist, + continuation, + appCache +) { + asyncOpenCacheEntry( + key, + where, + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(status, entry) { + if (shouldExist) { + dump("TEST-INFO | checking cache key " + key + " exists @ " + where); + Assert.equal(status, Cr.NS_OK); + Assert.ok(!!entry); + } else { + dump( + "TEST-INFO | checking cache key " + key + " doesn't exist @ " + where + ); + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + Assert.equal(null, entry); + } + continuation(); + }, + appCache + ); +} diff --git a/netwerk/test/unit/head_cache2.js b/netwerk/test/unit/head_cache2.js new file mode 100644 index 0000000000..5a531498ca --- /dev/null +++ b/netwerk/test/unit/head_cache2.js @@ -0,0 +1,428 @@ +/* import-globals-from head_cache.js */ +/* import-globals-from head_channels.js */ + +"use strict"; + +var callbacks = []; + +// Expect an existing entry +const NORMAL = 0; +// Expect a new entry +const NEW = 1 << 0; +// Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen +const NOTVALID = 1 << 1; +// Throw from onCacheEntryAvailable +const THROWAVAIL = 1 << 2; +// Open entry for reading-only +const READONLY = 1 << 3; +// Expect the entry to not be found +const NOTFOUND = 1 << 4; +// Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck +const REVAL = 1 << 5; +// Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry +const PARTIAL = 1 << 6; +// Expect the entry is doomed, i.e. the output stream should not be possible to open +const DOOMED = 1 << 7; +// Don't trigger the go-on callback until the entry is written +const WAITFORWRITE = 1 << 8; +// Don't write data (i.e. don't open output stream) +const METAONLY = 1 << 9; +// Do recreation of an existing cache entry +const RECREATE = 1 << 10; +// Do not give me the entry +const NOTWANTED = 1 << 11; +// Tell the cache to wait for the entry to be completely written first +const COMPLETE = 1 << 12; +// Don't write meta/data and don't set valid in the callback, consumer will do it manually +const DONTFILL = 1 << 13; +// Used in combination with METAONLY, don't call setValid() on the entry after metadata has been set +const DONTSETVALID = 1 << 14; +// Notify before checking the data, useful for proper callback ordering checks +const NOTIFYBEFOREREAD = 1 << 15; +// It's allowed to not get an existing entry (result of opening is undetermined) +const MAYBE_NEW = 1 << 16; + +var log_c2 = true; +function LOG_C2(o, m) { + if (!log_c2) { + return; + } + if (!m) { + dump("TEST-INFO | CACHE2: " + o + "\n"); + } else { + dump( + "TEST-INFO | CACHE2: callback #" + + o.order + + "(" + + (o.workingData ? o.workingData.substr(0, 10) : "---") + + ") " + + m + + "\n" + ); + } +} + +function pumpReadStream(inputStream, goon) { + if (inputStream.isNonBlocking()) { + // non-blocking stream, must read via pump + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(inputStream, 0, 0, true); + var data = ""; + pump.asyncRead({ + onStartRequest(aRequest) {}, + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + wrapper.init(aInputStream); + var str = wrapper.read(wrapper.available()); + LOG_C2("reading data '" + str.substring(0, 5) + "'"); + data += str; + }, + onStopRequest(aRequest, aStatusCode) { + LOG_C2("done reading data: " + aStatusCode); + Assert.equal(aStatusCode, Cr.NS_OK); + goon(data); + }, + }); + } else { + // blocking stream + var data = read_stream(inputStream, inputStream.available()); + goon(data); + } +} + +OpenCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), + onCacheEntryCheck(entry, appCache) { + LOG_C2(this, "onCacheEntryCheck"); + Assert.ok(!this.onCheckPassed); + this.onCheckPassed = true; + + if (this.behavior & NOTVALID) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + } + + if (this.behavior & NOTWANTED) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED; + } + + Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); + + // check for sane flag combination + Assert.notEqual(this.behavior & (REVAL | PARTIAL), REVAL | PARTIAL); + + if (this.behavior & (REVAL | PARTIAL)) { + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION"); + return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION; + } + + if (this.behavior & COMPLETE) { + LOG_C2( + this, + "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED" + ); + // Specific to the new backend because of concurrent read/write: + // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck + // the cache calls this callback again after the entry write has finished. + // This gives the consumer a chance to recheck completeness of the entry + // again. + // Thus, we reset state as onCheck would have never been called. + this.onCheckPassed = false; + // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck. + this.behavior &= ~COMPLETE; + return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED; + } + + LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable(entry, isnew, appCache, status) { + if (this.behavior & MAYBE_NEW && isnew) { + this.behavior |= NEW; + } + + LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); + Assert.ok(!this.onAvailPassed); + this.onAvailPassed = true; + + Assert.equal(isnew, !!(this.behavior & NEW)); + + if (this.behavior & (NOTFOUND | NOTWANTED)) { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + Assert.ok(!entry); + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + this.goon(entry); + } else if (this.behavior & (NEW | RECREATE)) { + Assert.ok(!!entry); + + if (this.behavior & RECREATE) { + entry = entry.recreate(); + Assert.ok(!!entry); + } + + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + + if (!(this.behavior & WAITFORWRITE)) { + this.goon(entry); + } + + if (!(this.behavior & PARTIAL)) { + try { + entry.getMetaDataElement("meto"); + Assert.ok(false); + } catch (ex) {} + } + + if (this.behavior & DONTFILL) { + Assert.equal(false, this.behavior & WAITFORWRITE); + return; + } + + var self = this; + executeSoon(function() { + // emulate network latency + entry.setMetaDataElement("meto", self.workingMetadata); + entry.metaDataReady(); + if (self.behavior & METAONLY) { + // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :( + if (!(self.behavior & DONTSETVALID)) { + entry.setValid(); + } + + entry.close(); + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + + return; + } + executeSoon(function() { + // emulate more network latency + if (self.behavior & DOOMED) { + LOG_C2(self, "checking doom state"); + try { + var os = entry.openOutputStream(0, -1); + // Unfortunately, in the undetermined state we cannot even check whether the entry + // is actually doomed or not. + os.close(); + Assert.ok(!!(self.behavior & MAYBE_NEW)); + } catch (ex) { + Assert.ok(true); + } + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + return; + } + + var offset = self.behavior & PARTIAL ? entry.dataSize : 0; + LOG_C2(self, "openOutputStream @ " + offset); + var os = entry.openOutputStream(offset, -1); + LOG_C2(self, "writing data"); + var wrt = os.write(self.workingData, self.workingData.length); + Assert.equal(wrt, self.workingData.length); + os.close(); + if (self.behavior & WAITFORWRITE) { + self.goon(entry); + } + + entry.close(); + }); + }); + } else { + // NORMAL + Assert.ok(!!entry); + Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); + if (this.behavior & THROWAVAIL) { + this.throwAndNotify(entry); + } + if (this.behavior & NOTIFYBEFOREREAD) { + this.goon(entry, true); + } + + var self = this; + pumpReadStream(entry.openInputStream(0), function(data) { + Assert.equal(data, self.workingData); + self.onDataCheckPassed = true; + LOG_C2(self, "entry read done"); + self.goon(entry); + entry.close(); + }); + } + }, + selfCheck() { + LOG_C2(this, "selfCheck"); + + Assert.ok(this.onCheckPassed || this.behavior & MAYBE_NEW); + Assert.ok(this.onAvailPassed); + Assert.ok(this.onDataCheckPassed || this.behavior & MAYBE_NEW); + }, + throwAndNotify(entry) { + LOG_C2(this, "Throwing"); + var self = this; + executeSoon(function() { + LOG_C2(self, "Notifying"); + self.goon(entry); + }); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + }, +}; + +function OpenCallback(behavior, workingMetadata, workingData, goon) { + this.behavior = behavior; + this.workingMetadata = workingMetadata; + this.workingData = workingData; + this.goon = goon; + this.onCheckPassed = + (!!(behavior & (NEW | RECREATE)) || !workingMetadata) && + !(behavior & NOTVALID); + this.onAvailPassed = false; + this.onDataCheckPassed = + !!(behavior & (NEW | RECREATE | NOTWANTED)) || !workingMetadata; + callbacks.push(this); + this.order = callbacks.length; +} + +VisitCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), + onCacheStorageInfo(num, consumption) { + LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); + Assert.equal(this.num, num); + Assert.equal(this.consumption, consumption); + if (!this.entries) { + this.notify(); + } + }, + onCacheEntryInfo( + aURI, + aIdEnhance, + aDataSize, + aFetchCount, + aLastModifiedTime, + aExpirationTime, + aPinned, + aInfo + ) { + var key = (aIdEnhance ? aIdEnhance + ":" : "") + aURI.asciiSpec; + LOG_C2(this, "onCacheEntryInfo: key=" + key); + + function findCacheIndex(element) { + if (typeof element === "string") { + return element === key; + } else if (typeof element === "object") { + return ( + element.uri === key && + element.lci.isAnonymous === aInfo.isAnonymous && + ChromeUtils.isOriginAttributesEqual( + element.lci.originAttributes, + aInfo.originAttributes + ) + ); + } + + return false; + } + + Assert.ok(!!this.entries); + + var index = this.entries.findIndex(findCacheIndex); + Assert.ok(index > -1); + + this.entries.splice(index, 1); + }, + onCacheEntryVisitCompleted() { + LOG_C2(this, "onCacheEntryVisitCompleted"); + if (this.entries) { + Assert.equal(this.entries.length, 0); + } + this.notify(); + }, + notify() { + Assert.ok(!!this.goon); + var goon = this.goon; + this.goon = null; + executeSoon(goon); + }, + selfCheck() { + Assert.ok(!this.entries || !this.entries.length); + }, +}; + +function VisitCallback(num, consumption, entries, goon) { + this.num = num; + this.consumption = consumption; + this.entries = entries; + this.goon = goon; + callbacks.push(this); + this.order = callbacks.length; +} + +EvictionCallback.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICacheEntryDoomCallback"]), + onCacheEntryDoomed(result) { + Assert.equal(this.expectedSuccess, result == Cr.NS_OK); + this.goon(); + }, + selfCheck() {}, +}; + +function EvictionCallback(success, goon) { + this.expectedSuccess = success; + this.goon = goon; + callbacks.push(this); + this.order = callbacks.length; +} + +MultipleCallbacks.prototype = { + fired() { + if (--this.pending == 0) { + var self = this; + if (this.delayed) { + executeSoon(function() { + self.goon(); + }); + } else { + this.goon(); + } + } + }, + add() { + ++this.pending; + }, +}; + +function MultipleCallbacks(number, goon, delayed) { + this.pending = number; + this.goon = goon; + this.delayed = delayed; +} + +function wait_for_cache_index(continue_func) { + // This callback will not fire before the index is in the ready state. nsICacheStorage.exists() will + // no longer throw after this point. + get_cache_service().asyncGetDiskConsumption({ + onNetworkCacheDiskConsumption() { + continue_func(); + }, + // eslint-disable-next-line mozilla/use-chromeutils-generateqi + QueryInterface() { + return this; + }, + }); +} + +function finish_cache2_test() { + callbacks.forEach(function(callback, index) { + callback.selfCheck(); + }); + do_test_finished(); +} diff --git a/netwerk/test/unit/head_channels.js b/netwerk/test/unit/head_channels.js new file mode 100644 index 0000000000..8761032238 --- /dev/null +++ b/netwerk/test/unit/head_channels.js @@ -0,0 +1,456 @@ +/** + * Read count bytes from stream and return as a String object + */ + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ + +function read_stream(stream, count) { + /* assume stream has non-ASCII data */ + var wrapper = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + wrapper.setInputStream(stream); + /* JS methods can be called with a maximum of 65535 arguments, and input + streams don't have to return all the data they make .available() when + asked to .read() that number of bytes. */ + var data = []; + while (count > 0) { + var bytes = wrapper.readByteArray(Math.min(65535, count)); + data.push(String.fromCharCode.apply(null, bytes)); + count -= bytes.length; + if (bytes.length == 0) { + do_throw("Nothing read from input stream!"); + } + } + return data.join(""); +} + +const CL_EXPECT_FAILURE = 0x1; +const CL_EXPECT_GZIP = 0x2; +const CL_EXPECT_3S_DELAY = 0x4; +const CL_SUSPEND = 0x8; +const CL_ALLOW_UNKNOWN_CL = 0x10; +const CL_EXPECT_LATE_FAILURE = 0x20; +const CL_FROM_CACHE = 0x40; // Response must be from the cache +const CL_NOT_FROM_CACHE = 0x80; // Response must NOT be from the cache +const CL_IGNORE_CL = 0x100; // don't bother to verify the content-length + +const SUSPEND_DELAY = 3000; + +/** + * A stream listener that calls a callback function with a specified + * context and the received data when the channel is loaded. + * + * Signature of the closure: + * void closure(in nsIRequest request, in ACString data, in JSObject context); + * + * This listener makes sure that various parts of the channel API are + * implemented correctly and that the channel's status is a success code + * (you can pass CL_EXPECT_FAILURE or CL_EXPECT_LATE_FAILURE as flags + * to allow a failure code) + * + * Note that it also requires a valid content length on the channel and + * is thus not fully generic. + */ +function ChannelListener(closure, ctx, flags) { + this._closure = closure; + this._closurectx = ctx; + this._flags = flags; + this._isFromCache = false; + this._cacheEntryId = undefined; +} +ChannelListener.prototype = { + _closure: null, + _closurectx: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + _lastEvent: 0, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + try { + if (this._got_onstartrequest) { + do_throw("Got second onStartRequest event!"); + } + this._got_onstartrequest = true; + this._lastEvent = Date.now(); + + try { + this._isFromCache = request + .QueryInterface(Ci.nsICacheInfoChannel) + .isFromCache(); + } catch (e) {} + + var thrown = false; + try { + this._cacheEntryId = request + .QueryInterface(Ci.nsICacheInfoChannel) + .getCacheEntryId(); + } catch (e) { + thrown = true; + } + if (this._isFromCache && thrown) { + do_throw("Should get a CacheEntryId"); + } else if (!this._isFromCache && !thrown) { + do_throw("Shouldn't get a CacheEntryId"); + } + + request.QueryInterface(Ci.nsIChannel); + try { + this._contentLen = request.contentLength; + } catch (ex) { + if (!(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL))) { + do_throw("Could not get contentLength"); + } + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onStartRequest!"); + } + if ( + this._contentLen == -1 && + !(this._flags & (CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL)) + ) { + do_throw("Content length is unknown in onStartRequest!"); + } + + if (this._flags & CL_FROM_CACHE) { + request.QueryInterface(Ci.nsICachingChannel); + if (!request.isFromCache()) { + do_throw("Response is not from the cache (CL_FROM_CACHE)"); + } + } + if (this._flags & CL_NOT_FROM_CACHE) { + request.QueryInterface(Ci.nsICachingChannel); + if (request.isFromCache()) { + do_throw("Response is from the cache (CL_NOT_FROM_CACHE)"); + } + } + + if (this._flags & CL_SUSPEND) { + request.suspend(); + do_timeout(SUSPEND_DELAY, function() { + request.resume(); + }); + } + } catch (ex) { + do_throw("Error in onStartRequest: " + ex); + } + }, + + onDataAvailable(request, stream, offset, count) { + try { + let current = Date.now(); + + if (!this._got_onstartrequest) { + do_throw("onDataAvailable without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("onDataAvailable after onStopRequest event!"); + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onDataAvailable!"); + } + if (this._flags & CL_EXPECT_FAILURE) { + do_throw("Got data despite expecting a failure"); + } + + if ( + current - this._lastEvent >= SUSPEND_DELAY && + !(this._flags & CL_EXPECT_3S_DELAY) + ) { + do_throw("Data received after significant unexpected delay"); + } else if ( + current - this._lastEvent < SUSPEND_DELAY && + this._flags & CL_EXPECT_3S_DELAY + ) { + do_throw("Data received sooner than expected"); + } else if ( + current - this._lastEvent >= SUSPEND_DELAY && + this._flags & CL_EXPECT_3S_DELAY + ) { + this._flags &= ~CL_EXPECT_3S_DELAY; + } // No more delays expected + + this._buffer = this._buffer.concat(read_stream(stream, count)); + this._lastEvent = current; + } catch (ex) { + do_throw("Error in onDataAvailable: " + ex); + } + }, + + onStopRequest(request, status) { + try { + var success = Components.isSuccessCode(status); + if (!this._got_onstartrequest) { + do_throw("onStopRequest without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("Got second onStopRequest event!"); + } + this._got_onstoprequest = true; + if ( + this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE) && + success + ) { + do_throw( + "Should have failed to load URL (status is " + + status.toString(16) + + ")" + ); + } else if ( + !(this._flags & (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE)) && + !success + ) { + do_throw("Failed to load URL: " + status.toString(16)); + } + if (status != request.status) { + do_throw("request.status does not match status arg to onStopRequest!"); + } + if (request.isPending()) { + do_throw("request reports itself as pending from onStopRequest!"); + } + if ( + !( + this._flags & + (CL_EXPECT_FAILURE | CL_EXPECT_LATE_FAILURE | CL_IGNORE_CL) + ) && + !(this._flags & CL_EXPECT_GZIP) && + this._contentLen != -1 + ) { + Assert.equal(this._buffer.length, this._contentLen); + } + } catch (ex) { + do_throw("Error in onStopRequest: " + ex); + } + try { + this._closure( + request, + this._buffer, + this._closurectx, + this._isFromCache, + this._cacheEntryId + ); + this._closurectx = null; + } catch (ex) { + do_throw("Error in closure function: " + ex); + } + }, +}; + +var ES_ABORT_REDIRECT = 0x01; + +function ChannelEventSink(flags) { + this._flags = flags; +} + +ChannelEventSink.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + if (this._flags & ES_ABORT_REDIRECT) { + throw Components.Exception("", Cr.NS_BINDING_ABORTED); + } + + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, +}; + +/** + * A helper class to construct origin attributes. + */ +function OriginAttributes(inIsolatedMozBrowser, privateId) { + this.inIsolatedMozBrowser = inIsolatedMozBrowser; + this.privateBrowsingId = privateId; +} +OriginAttributes.prototype = { + inIsolatedMozBrowser: false, + privateBrowsingId: 0, +}; + +function readFile(file) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + let data = NetUtil.readInputStreamToString(fstream, fstream.available()); + fstream.close(); + return data; +} + +function addCertFromFile(certdb, filename, trustString) { + let certFile = do_get_file(filename, false); + let pem = readFile(certFile) + .replace(/-----BEGIN CERTIFICATE-----/, "") + .replace(/-----END CERTIFICATE-----/, "") + .replace(/[\r\n]/g, ""); + certdb.addCertFromBase64(pem, trustString); +} + +// Helper code to test nsISerializable +function serialize_to_escaped_string(obj) { + let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeCompoundObject(obj, Ci.nsISupports, true); + objectOutStream.close(); + + let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + let data = []; + // This reads all the data from the stream until an error occurs. + while (true) { + try { + let bytes = objectInStream.readByteArray(1); + data.push(String.fromCharCode.apply(null, bytes)); + } catch (e) { + break; + } + } + return escape(data.join("")); +} + +function deserialize_from_escaped_string(str) { + let payload = unescape(str); + let data = []; + let i = 0; + while (i < payload.length) { + data.push(payload.charCodeAt(i++)); + } + + let objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeByteArray(data); + objectOutStream.close(); + + let objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + return objectInStream.readObject(true); +} + +// Copied from head_psm.js. +function add_tls_server_setup(serverBinName, certsPath, addDefaultRoot = true) { + add_test(function() { + _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot); + }); +} + +// Do not call this directly; use add_tls_server_setup +function _setupTLSServerTest(serverBinName, certsPath, addDefaultRoot) { + asyncStartTLSTestServer(serverBinName, certsPath, addDefaultRoot).then( + run_next_test + ); +} + +async function asyncStartTLSTestServer( + serverBinName, + certsPath, + addDefaultRoot +) { + const { HttpServer } = ChromeUtils.import( + "resource://testing-common/httpd.js" + ); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + // The trusted CA that is typically used for "good" certificates. + if (addDefaultRoot) { + addCertFromFile(certdb, `${certsPath}/test-ca.pem`, "CTu,u,u"); + } + + const CALLBACK_PORT = 8444; + + let envSvc = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let greBinDir = Services.dirsvc.get("GreBinD", Ci.nsIFile); + envSvc.set("DYLD_LIBRARY_PATH", greBinDir.path); + // TODO(bug 1107794): Android libraries are in /data/local/xpcb, but "GreBinD" + // does not return this path on Android, so hard code it here. + envSvc.set("LD_LIBRARY_PATH", greBinDir.path + ":/data/local/xpcb"); + envSvc.set("MOZ_TLS_SERVER_DEBUG_LEVEL", "3"); + envSvc.set("MOZ_TLS_SERVER_CALLBACK_PORT", CALLBACK_PORT); + + let httpServer = new HttpServer(); + let serverReady = new Promise(resolve => { + httpServer.registerPathHandler("/", function handleServerCallback( + aRequest, + aResponse + ) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain"); + let responseBody = "OK!"; + aResponse.bodyOutputStream.write(responseBody, responseBody.length); + executeSoon(function() { + httpServer.stop(resolve); + }); + }); + httpServer.start(CALLBACK_PORT); + }); + + let serverBin = _getBinaryUtil(serverBinName); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(serverBin); + let certDir = do_get_file(certsPath, false); + Assert.ok(certDir.exists(), `certificate folder (${certsPath}) should exist`); + // Using "sql:" causes the SQL DB to be used so we can run tests on Android. + process.run(false, ["sql:" + certDir.path, Services.appinfo.processID], 2); + + registerCleanupFunction(function() { + process.kill(); + }); + + await serverReady; +} + +function _getBinaryUtil(binaryUtilName) { + let utilBin = Services.dirsvc.get("GreD", Ci.nsIFile); + // On macOS, GreD is .../Contents/Resources, and most binary utilities + // are located there, but certutil is in GreBinD (or .../Contents/MacOS), + // so we have to change the path accordingly. + if (binaryUtilName === "certutil") { + utilBin = Services.dirsvc.get("GreBinD", Ci.nsIFile); + } + utilBin.append(binaryUtilName + mozinfo.bin_suffix); + // If we're testing locally, the above works. If not, the server executable + // is in another location. + if (!utilBin.exists()) { + utilBin = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + while (utilBin.path.includes("xpcshell")) { + utilBin = utilBin.parent; + } + utilBin.append("bin"); + utilBin.append(binaryUtilName + mozinfo.bin_suffix); + } + // But maybe we're on Android, where binaries are in /data/local/xpcb. + if (!utilBin.exists()) { + utilBin.initWithPath("/data/local/xpcb/"); + utilBin.append(binaryUtilName); + } + Assert.ok(utilBin.exists(), `Binary util ${binaryUtilName} should exist`); + return utilBin; +} diff --git a/netwerk/test/unit/head_cookies.js b/netwerk/test/unit/head_cookies.js new file mode 100644 index 0000000000..feb6939b4f --- /dev/null +++ b/netwerk/test/unit/head_cookies.js @@ -0,0 +1,973 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* import-globals-from head_cache.js */ + +"use strict"; + +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { CookieXPCShellUtils } = ChromeUtils.import( + "resource://testing-common/CookieXPCShellUtils.jsm" +); + +// Don't pick up default permissions from profile. +Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); + +CookieXPCShellUtils.init(this); + +XPCOMUtils.defineLazyServiceGetter( + Services, + "cookiesvc", + "@mozilla.org/cookieService;1", + "nsICookieService" +); +XPCOMUtils.defineLazyServiceGetter( + Services, + "cookiemgr", + "@mozilla.org/cookiemanager;1", + "nsICookieManager" +); + +XPCOMUtils.defineLazyServiceGetter( + Services, + "etld", + "@mozilla.org/network/effective-tld-service;1", + "nsIEffectiveTLDService" +); + +function do_check_throws(f, result, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + try { + f(); + } catch (exc) { + if (exc.result == result) { + return; + } + do_throw("expected result " + result + ", caught " + exc, stack); + } + do_throw("expected result " + result + ", none thrown", stack); +} + +// Helper to step a generator function and catch a StopIteration exception. +function do_run_generator(generator) { + try { + generator.next(); + } catch (e) { + do_throw("caught exception " + e, Components.stack.caller); + } +} + +// Helper to finish a generator function test. +function do_finish_generator_test(generator) { + executeSoon(function() { + generator.return(); + do_test_finished(); + }); +} + +function _observer(generator, topic) { + Services.obs.addObserver(this, topic); + + this.generator = generator; + this.topic = topic; +} + +_observer.prototype = { + observe(subject, topic, data) { + Assert.equal(this.topic, topic); + + Services.obs.removeObserver(this, this.topic); + + // Continue executing the generator function. + if (this.generator) { + do_run_generator(this.generator); + } + + this.generator = null; + this.topic = null; + }, +}; + +// Close the cookie database. If a generator is supplied, it will be invoked +// once the close is complete. +function do_close_profile(generator) { + // Register an observer for db close. + new _observer(generator, "cookie-db-closed"); + + // Close the db. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-before-change", "shutdown-persist"); +} + +function _promise_observer(topic) { + Services.obs.addObserver(this, topic); + + this.topic = topic; + return new Promise(resolve => (this.resolve = resolve)); +} + +_promise_observer.prototype = { + observe(subject, topic, data) { + Assert.equal(this.topic, topic); + + Services.obs.removeObserver(this, this.topic); + if (this.resolve) { + this.resolve(); + } + + this.resolve = null; + this.topic = null; + }, +}; + +// Close the cookie database. And resolve a promise. +function promise_close_profile() { + // Register an observer for db close. + let promise = new _promise_observer("cookie-db-closed"); + + // Close the db. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-before-change", "shutdown-persist"); + + return promise; +} + +// Load the cookie database. +function promise_load_profile() { + // Register an observer for read completion. + let promise = new _promise_observer("cookie-db-read"); + + // Load the profile. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-do-change", ""); + + return promise; +} + +// Load the cookie database. If a generator is supplied, it will be invoked +// once the load is complete. +function do_load_profile(generator) { + // Register an observer for read completion. + new _observer(generator, "cookie-db-read"); + + // Load the profile. + let service = Services.cookies.QueryInterface(Ci.nsIObserver); + service.observe(null, "profile-do-change", ""); +} + +// Set a single session cookie using http and test the cookie count +// against 'expected' +function do_set_single_http_cookie(uri, channel, expected) { + Services.cookiesvc.setCookieStringFromHttp(uri, "foo=bar", channel); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected); +} + +// Set two cookies; via document.channel and via http request. +async function do_set_cookies(uri, channel, session, expected) { + let suffix = session ? "" : "; max-age=1000"; + + // via document.cookie + const thirdPartyUrl = "http://third.com/"; + const contentPage = await CookieXPCShellUtils.loadContentPage(thirdPartyUrl); + await contentPage.spawn( + { + cookie: "can=has" + suffix, + url: uri.spec, + }, + async obj => { + // eslint-disable-next-line no-undef + await new content.Promise(resolve => { + // eslint-disable-next-line no-undef + const ifr = content.document.createElement("iframe"); + // eslint-disable-next-line no-undef + content.document.body.appendChild(ifr); + ifr.src = obj.url; + ifr.onload = () => { + ifr.contentDocument.cookie = obj.cookie; + resolve(); + }; + }); + } + ); + await contentPage.close(); + + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected[0]); + + // via http request + Services.cookiesvc.setCookieStringFromHttp(uri, "hot=dog" + suffix, channel); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri.host), expected[1]); +} + +function do_count_cookies() { + return Services.cookiemgr.cookies.length; +} + +// Helper object to store cookie data. +function Cookie( + name, + value, + host, + path, + expiry, + lastAccessed, + creationTime, + isSession, + isSecure, + isHttpOnly, + inBrowserElement = false, + originAttributes = {}, + sameSite = Ci.nsICookie.SAMESITE_NONE, + rawSameSite = Ci.nsICookie.SAMESITE_NONE, + schemeMap = Ci.nsICookie.SCHEME_UNSET +) { + this.name = name; + this.value = value; + this.host = host; + this.path = path; + this.expiry = expiry; + this.lastAccessed = lastAccessed; + this.creationTime = creationTime; + this.isSession = isSession; + this.isSecure = isSecure; + this.isHttpOnly = isHttpOnly; + this.inBrowserElement = inBrowserElement; + this.originAttributes = originAttributes; + this.sameSite = sameSite; + this.rawSameSite = rawSameSite; + this.schemeMap = schemeMap; + + let strippedHost = host.charAt(0) == "." ? host.slice(1) : host; + + try { + this.baseDomain = Services.etld.getBaseDomainFromHost(strippedHost); + } catch (e) { + if ( + e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS || + e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS + ) { + this.baseDomain = strippedHost; + } + } +} + +// Object representing a database connection and associated statements. The +// implementation varies depending on schema version. +function CookieDatabaseConnection(file, schema) { + // Manually generate a cookies.sqlite file with appropriate rows, columns, + // and schema version. If it already exists, just set up our statements. + let exists = file.exists(); + + this.db = Services.storage.openDatabase(file); + this.schema = schema; + if (!exists) { + this.db.schemaVersion = schema; + } + + switch (schema) { + case 1: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + id, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + break; + } + + case 2: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT OR REPLACE INTO moz_cookies ( \ + id, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id" + ); + + break; + } + + case 3: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER)" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + id, \ + baseDomain, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :id, \ + :baseDomain, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies WHERE id = :id" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id" + ); + + break; + } + + case 4: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path))" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + baseDomain, \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly) \ + VALUES ( \ + :baseDomain, \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path" + ); + + break; + } + + case 10: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + baseDomain TEXT, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + baseDomain, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :baseDomain, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + case 11: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + case 12: { + if (!exists) { + this.db.executeSimpleSQL( + "CREATE TABLE moz_cookies ( \ + id INTEGER PRIMARY KEY, \ + originAttributes TEXT NOT NULL DEFAULT '', \ + name TEXT, \ + value TEXT, \ + host TEXT, \ + path TEXT, \ + expiry INTEGER, \ + lastAccessed INTEGER, \ + creationTime INTEGER, \ + isSecure INTEGER, \ + isHttpOnly INTEGER, \ + inBrowserElement INTEGER DEFAULT 0, \ + sameSite INTEGER DEFAULT 0, \ + rawSameSite INTEGER DEFAULT 0, \ + schemeMap INTEGER DEFAULT 0, \ + CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))" + ); + + this.db.executeSimpleSQL("PRAGMA journal_mode = WAL"); + this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + } + + this.stmtInsert = this.db.createStatement( + "INSERT INTO moz_cookies ( \ + name, \ + value, \ + host, \ + path, \ + expiry, \ + lastAccessed, \ + creationTime, \ + isSecure, \ + isHttpOnly, \ + inBrowserElement, \ + originAttributes, \ + sameSite, \ + rawSameSite, \ + schemeMap \ + ) VALUES ( \ + :name, \ + :value, \ + :host, \ + :path, \ + :expiry, \ + :lastAccessed, \ + :creationTime, \ + :isSecure, \ + :isHttpOnly, \ + :inBrowserElement, \ + :originAttributes, \ + :sameSite, \ + :rawSameSite, \ + :schemeMap)" + ); + + this.stmtDelete = this.db.createStatement( + "DELETE FROM moz_cookies \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + this.stmtUpdate = this.db.createStatement( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed \ + WHERE name = :name AND host = :host AND path = :path AND \ + originAttributes = :originAttributes" + ); + + break; + } + + default: + do_throw("unrecognized schemaVersion!"); + } +} + +CookieDatabaseConnection.prototype = { + insertCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.schema) { + case 1: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 2: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 3: + this.stmtInsert.bindByName("id", cookie.creationTime); + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 4: + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + break; + + case 10: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("baseDomain", cookie.baseDomain); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + break; + + case 11: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + break; + + case 12: + this.stmtInsert.bindByName("name", cookie.name); + this.stmtInsert.bindByName("value", cookie.value); + this.stmtInsert.bindByName("host", cookie.host); + this.stmtInsert.bindByName("path", cookie.path); + this.stmtInsert.bindByName("expiry", cookie.expiry); + this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed); + this.stmtInsert.bindByName("creationTime", cookie.creationTime); + this.stmtInsert.bindByName("isSecure", cookie.isSecure); + this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly); + this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement); + this.stmtInsert.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtInsert.bindByName("sameSite", cookie.sameSite); + this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite); + this.stmtInsert.bindByName("schemeMap", cookie.schemeMap); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtInsert); + }, + + deleteCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.db.schemaVersion) { + case 1: + case 2: + case 3: + this.stmtDelete.bindByName("id", cookie.creationTime); + break; + + case 4: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + break; + + case 10: + case 11: + case 12: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtDelete.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtDelete); + }, + + updateCookie(cookie) { + if (!(cookie instanceof Cookie)) { + do_throw("not a cookie"); + } + + switch (this.db.schemaVersion) { + case 1: + do_throw("can't update a schema 1 cookie!"); + break; + case 2: + case 3: + this.stmtUpdate.bindByName("id", cookie.creationTime); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + case 4: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtUpdate.bindByName("name", cookie.name); + this.stmtUpdate.bindByName("host", cookie.host); + this.stmtUpdate.bindByName("path", cookie.path); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + case 10: + case 11: + case 12: + this.stmtDelete.bindByName("name", cookie.name); + this.stmtDelete.bindByName("host", cookie.host); + this.stmtDelete.bindByName("path", cookie.path); + this.stmtDelete.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtUpdate.bindByName("name", cookie.name); + this.stmtUpdate.bindByName("host", cookie.host); + this.stmtUpdate.bindByName("path", cookie.path); + this.stmtUpdate.bindByName( + "originAttributes", + ChromeUtils.originAttributesToSuffix(cookie.originAttributes) + ); + this.stmtUpdate.bindByName("lastAccessed", cookie.lastAccessed); + break; + + default: + do_throw("unrecognized schemaVersion!"); + } + + do_execute_stmt(this.stmtUpdate); + }, + + close() { + this.stmtInsert.finalize(); + this.stmtDelete.finalize(); + if (this.stmtUpdate) { + this.stmtUpdate.finalize(); + } + this.db.close(); + + this.stmtInsert = null; + this.stmtDelete = null; + this.stmtUpdate = null; + this.db = null; + }, +}; + +function do_get_cookie_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite"); + return file; +} + +// Count the cookies from 'host' in a database. If 'host' is null, count all +// cookies. +function do_count_cookies_in_db(connection, host) { + let select = null; + if (host) { + select = connection.createStatement( + "SELECT COUNT(1) FROM moz_cookies WHERE host = :host" + ); + select.bindByName("host", host); + } else { + select = connection.createStatement("SELECT COUNT(1) FROM moz_cookies"); + } + + select.executeStep(); + let result = select.getInt32(0); + select.reset(); + select.finalize(); + return result; +} + +// Execute 'stmt', ensuring that we reset it if it throws. +function do_execute_stmt(stmt) { + try { + stmt.executeStep(); + stmt.reset(); + } catch (e) { + stmt.reset(); + throw e; + } +} diff --git a/netwerk/test/unit/head_http3.js b/netwerk/test/unit/head_http3.js new file mode 100644 index 0000000000..9fffa72ea4 --- /dev/null +++ b/netwerk/test/unit/head_http3.js @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from head_channels.js */ +/* import-globals-from head_cookies.js */ + +async function http3_setup_tests() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + let h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + let h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=:" + h3Port + ); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + await setup_altsvc("https://foo.example.com/", h3Route); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let CheckHttp3Listener = function() {}; + +CheckHttp3Listener.prototype = { + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + this.finish(true); + } else { + dump("try again to get alt svc mapping\n"); + this.finish(false); + } + }, +}; + +async function setup_altsvc(uri, expectedRoute) { + let result = false; + do { + let chan = makeChan(uri); + let listener = new CheckHttp3Listener(); + listener.expectedRoute = expectedRoute; + result = await altsvcSetupPromise(chan, listener); + dump("results=" + result); + } while (result === false); +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function http3_clear_prefs() { + Services.prefs.clearUserPref("network.http.http3.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + dump("cleanup done\n"); +} diff --git a/netwerk/test/unit/head_trr.js b/netwerk/test/unit/head_trr.js new file mode 100644 index 0000000000..bc488ec6cc --- /dev/null +++ b/netwerk/test/unit/head_trr.js @@ -0,0 +1,364 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from head_cache.js */ +/* import-globals-from head_cookies.js */ +/* import-globals-from head_channels.js */ + +/* globals require, __dirname, global, Buffer */ + +const { NodeServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/// Sets the TRR related prefs and adds the certificate we use for the HTTP2 +/// server. +function trr_test_setup() { + dump("start!\n"); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.spdy.enabled", true); + Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + Services.prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // use the h2 server as DOH provider + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - reserved, 2 - TRR first, 3 - TRR only, 4 - reserved + Services.prefs.setIntPref("network.trr.mode", 2); // TRR first + Services.prefs.setBoolPref("network.trr.wait-for-portal", false); + // By default wait for all responses before notifying the listeners. + Services.prefs.setBoolPref("network.trr.wait-for-A-and-AAAA", true); + // don't confirm that TRR is working, just go! + Services.prefs.setCharPref("network.trr.confirmationNS", "skip"); + // some tests rely on the cache not being cleared on pref change. + // we specifically test that this works + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +/// Clears the prefs that we're likely to set while testing TRR code +function trr_clear_prefs() { + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + Services.prefs.clearUserPref("network.trr.credentials"); + Services.prefs.clearUserPref("network.trr.wait-for-portal"); + Services.prefs.clearUserPref("network.trr.allow-rfc1918"); + Services.prefs.clearUserPref("network.trr.useGET"); + Services.prefs.clearUserPref("network.trr.confirmationNS"); + Services.prefs.clearUserPref("network.trr.bootstrapAddress"); + Services.prefs.clearUserPref("network.trr.blacklist-duration"); + Services.prefs.clearUserPref("network.trr.request_timeout_ms"); + Services.prefs.clearUserPref("network.trr.request_timeout_mode_trronly_ms"); + Services.prefs.clearUserPref("network.trr.disable-ECS"); + Services.prefs.clearUserPref("network.trr.early-AAAA"); + Services.prefs.clearUserPref("network.trr.skip-AAAA-when-not-supported"); + Services.prefs.clearUserPref("network.trr.wait-for-A-and-AAAA"); + Services.prefs.clearUserPref("network.trr.excluded-domains"); + Services.prefs.clearUserPref("network.trr.builtin-excluded-domains"); + Services.prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + Services.prefs.clearUserPref("network.trr.fetch_off_main_thread"); + Services.prefs.clearUserPref("captivedetect.canonicalURL"); + + Services.prefs.clearUserPref("network.http.spdy.enabled"); + Services.prefs.clearUserPref("network.http.spdy.enabled.http2"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + Services.prefs.clearUserPref( + "network.trr.send_empty_accept-encoding_headers" + ); +} + +/// This class sends a DNS query and can be awaited as a promise to get the +/// response. +class TRRDNSListener { + constructor(name, options = {}) { + this.name = name; + this.options = options; + this.expectedAnswer = options.expectedAnswer ?? undefined; + this.expectedSuccess = options.expectedSuccess ?? true; + this.delay = options.delay; + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + let trrServer = options.trrServer || ""; + + const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService + ); + const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager + ); + const currentThread = threadManager.currentThread; + + let resolverInfo = + trrServer == "" ? null : dns.newTRRResolverInfo(trrServer); + try { + this.request = dns.asyncResolve( + name, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + this.options.flags || 0, + resolverInfo, + this, + currentThread, + {} // defaultOriginAttributes + ); + Assert.ok(!options.expectEarlyFail); + } catch (e) { + Assert.ok(options.expectEarlyFail); + this.resolve([e]); + } + } + + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok( + inRequest == this.request, + "Checking that this is the correct callback" + ); + + // If we don't expect success here, just resolve and the caller will + // decide what to do with the results. + if (!this.expectedSuccess) { + this.resolve([inRequest, inRecord, inStatus]); + return; + } + + Assert.equal(inStatus, Cr.NS_OK, "Checking status"); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.equal( + answer, + this.expectedAnswer, + `Checking result for ${this.name}` + ); + inRecord.rewind(); // In case the caller also checks the addresses + + if (this.delay !== undefined) { + Assert.greaterOrEqual( + inRecord.trrFetchDurationNetworkOnly, + this.delay, + `the response should take at least ${this.delay}` + ); + + Assert.greaterOrEqual( + inRecord.trrFetchDuration, + this.delay, + `the response should take at least ${this.delay}` + ); + + if (this.delay == 0) { + // The response timing should be really 0 + Assert.equal( + inRecord.trrFetchDurationNetworkOnly, + 0, + `the response time should be 0` + ); + + Assert.equal( + inRecord.trrFetchDuration, + this.delay, + `the response time should be 0` + ); + } + } + + this.resolve([inRequest, inRecord, inStatus]); + } + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsIDNSListener) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + + // Implement then so we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +/// Implements a basic HTTP2 server +class TRRServerCode { + static async startServer(port) { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + + const url = require("url"); + global.path_handlers = {}; + global.handler = (req, resp) => { + const path = req.headers[global.http2.constants.HTTP2_HEADER_PATH]; + let u = url.parse(req.url, true); + let handler = global.path_handlers[u.pathname]; + if (handler) { + return handler(req, resp, u); + } + + // Didn't find a handler for this path. + let response = `

404 Path not found: ${path}

`; + resp.setHeader("Content-Type", "text/html"); + resp.setHeader("Content-Length", response.length); + resp.writeHead(404); + resp.end(response); + }; + + // key: string "name/type" + // value: array [answer1, answer2] + global.dns_query_answers = {}; + + global.http2 = require("http2"); + global.server = global.http2.createSecureServer(options, global.handler); + + await global.server.listen(port); + + global.dnsPacket = require(`${__dirname}/../dns-packet`); + global.ip = require(`${__dirname}/../node-ip`); + + return global.server.address().port; + } +} + +/// This is the default handler for /dns-query +/// It implements basic functionality for parsing the DoH packet, then +/// queries global.dns_query_answers for available answers for the DNS query. +function trrQueryHandler(req, resp, url) { + let requestBody = Buffer.from(""); + let method = req.headers[global.http2.constants.HTTP2_HEADER_METHOD]; + let contentLength = req.headers["content-length"]; + + if (method == "POST") { + req.on("data", chunk => { + requestBody = Buffer.concat([requestBody, chunk]); + if (requestBody.length == contentLength) { + return processRequest(req, resp, requestBody); + } + }); + } else if (method == "GET") { + if (!url.query.dns) { + resp.writeHead(400); + resp.end("Missing dns parameter"); + return; + } + + requestBody = Buffer.from(url.query.dns, "base64"); + return processRequest(req, resp, requestBody); + } else { + // unexpected method. + resp.writeHead(405); + resp.end("Unexpected method"); + } + + function processRequest(req, resp, payload) { + let dnsQuery = global.dnsPacket.decode(payload); + let response = + global.dns_query_answers[ + `${dnsQuery.questions[0].name}/${dnsQuery.questions[0].type}` + ] || {}; + + let flags = global.dnsPacket.RECURSION_DESIRED; + if ( + (!response.answers || !response.answers.length) && + response.additionals && + response.additionals.length > 0 + ) { + flags |= global.dnsPacket.rcodes.toRcode("SERVFAIL"); + } + let buf = global.dnsPacket.encode({ + type: "response", + id: dnsQuery.id, + flags, + questions: dnsQuery.questions, + answers: response.answers || [], + additionals: response.additionals || [], + }); + + let writeResponse = (resp, buf) => { + resp.setHeader("Content-Length", buf.length); + resp.writeHead(200, { "Content-Type": "application/dns-message" }); + resp.write(buf); + resp.end(""); + }; + + if (response.delay) { + setTimeout( + arg => { + writeResponse(arg[0], arg[1]); + }, + response.delay, + [resp, buf] + ); + return; + } + + writeResponse(resp, buf); + } +} + +// A convenient wrapper around NodeServer +class TRRServer { + /// Starts the server + /// @port - default 0 + /// when provided, will attempt to listen on that port. + async start(port = 0) { + this.processId = await NodeServer.fork(); + + await this.execute(TRRServerCode); + this.port = await this.execute(`TRRServerCode.startServer(${port})`); + await this.registerPathHandler("/dns-query", trrQueryHandler); + } + + /// Executes a command in the context of the node server + async execute(command) { + return NodeServer.execute(this.processId, command); + } + + /// Stops the server + async stop() { + if (this.processId) { + await NodeServer.kill(this.processId); + this.processId = undefined; + } + } + + /// @path : string - the path on the server that we're handling. ex: /path + /// @handler : function(req, resp, url) - function that processes request and + /// emits a response. + async registerPathHandler(path, handler) { + return this.execute( + `global.path_handlers["${path}"] = ${handler.toString()}` + ); + } + + /// @name : string - name we're providing answers for. eg: foo.example.com + /// @type : string - the DNS query type. eg: "A", "AAAA", "CNAME", etc + /// @answers : array - array of answers (hashmap) that dnsPacket can parse + /// eg: [{ + /// name: "bar.example.com", + /// ttl: 55, + /// type: "A", + /// flush: false, + /// data: "1.2.3.4", + /// }] + async registerDoHAnswers(name, type, answers, additionals, delay = 0) { + let text = `global.dns_query_answers["${name}/${type}"] = ${JSON.stringify({ + answers, + additionals, + delay, + })}`; + return this.execute(text); + } +} diff --git a/netwerk/test/unit/http2-ca.pem b/netwerk/test/unit/http2-ca.pem new file mode 100644 index 0000000000..ef5a801720 --- /dev/null +++ b/netwerk/test/unit/http2-ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAbygAwIBAgIURZvN7yVqFNwThGHASoy1OlOGvOMwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwIhgPMjAxNzAxMDEwMDAwMDBa +GA8yMDI3MDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT +2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV +JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N +jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA +BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh +He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB +AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADyDiQnKjsvR +NrOk0aqgJ8XgK/IgJXFLbAVivjBLwnJGEkwxrFtC14mpTrPuXw9AybhroMjinq4Y +cNYTFuTE34k0fZEU8d60J/Tpfd1i0EB8+oUPuqOn+N29/LeHPAnkDJdOZye3w0U+ +StAI79WqUYQaKIG7qLnt60dQwBte12uvbuPaB3mREIfDXOKcjLBdZHL1waWjtzUX +z2E91VIdpvJGfEfXC3fIe1uO9Jh/E9NVWci84+njkNsl+OyBfOJ8T+pV3SHfWedp +Zbjwh6UTukIuc3mW0rS/qZOa2w3HQaO53BMbluo0w1+cscOepsATld2HHvSiHB+0 +K8SWFRHdBOU= +-----END CERTIFICATE----- diff --git a/netwerk/test/unit/http2-ca.pem.certspec b/netwerk/test/unit/http2-ca.pem.certspec new file mode 100644 index 0000000000..46f62e3fbc --- /dev/null +++ b/netwerk/test/unit/http2-ca.pem.certspec @@ -0,0 +1,4 @@ +issuer: HTTP2 Test CA +subject: HTTP2 Test CA +validity:20170101-20270101 +extension:basicConstraints:cA, diff --git a/netwerk/test/unit/moz.build b/netwerk/test/unit/moz.build new file mode 100644 index 0000000000..2e6708eaf6 --- /dev/null +++ b/netwerk/test/unit/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Temporarily disabled. See bug 1256495. +# GeneratedTestCertificate('http2-ca.pem') diff --git a/netwerk/test/unit/perftest.ini b/netwerk/test/unit/perftest.ini new file mode 100644 index 0000000000..789d6ae3e9 --- /dev/null +++ b/netwerk/test/unit/perftest.ini @@ -0,0 +1 @@ +[test_http3_perf.js] diff --git a/netwerk/test/unit/socks_client_subprocess.js b/netwerk/test/unit/socks_client_subprocess.js new file mode 100644 index 0000000000..1ae0e31490 --- /dev/null +++ b/netwerk/test/unit/socks_client_subprocess.js @@ -0,0 +1,101 @@ +/* global arguments */ + +"use strict"; + +var CC = Components.Constructor; + +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const ProtocolProxyService = CC( + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService" +); +var sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService +); + +function waitForStream(stream, streamType) { + return new Promise((resolve, reject) => { + stream = stream.QueryInterface(streamType); + if (!stream) { + reject("stream didn't implement given stream type"); + } + let currentThread = Cc["@mozilla.org/thread-manager;1"].getService() + .currentThread; + stream.asyncWait( + stream => { + resolve(stream); + }, + 0, + 0, + currentThread + ); + }); +} + +async function launchConnection( + socks_vers, + socks_port, + dest_host, + dest_port, + dns +) { + let pi_flags = 0; + if (dns == "remote") { + pi_flags = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST; + } + + let pps = new ProtocolProxyService(); + let pi = pps.newProxyInfo( + socks_vers, + "localhost", + socks_port, + "", + "", + pi_flags, + -1, + null + ); + let trans = sts.createTransport([], dest_host, dest_port, pi); + let input = trans.openInputStream(0, 0, 0); + let output = trans.openOutputStream(0, 0, 0); + input = await waitForStream(input, Ci.nsIAsyncInputStream); + let bin = new BinaryInputStream(input); + let data = bin.readBytes(5); + let response; + if (data == "PING!") { + print("client: got ping, sending pong."); + response = "PONG!"; + } else { + print("client: wrong data from server:", data); + response = "Error: wrong data received."; + } + output = await waitForStream(output, Ci.nsIAsyncOutputStream); + output.write(response, response.length); + output.close(); + input.close(); +} + +async function run(args) { + for (let arg of args) { + print("client: running test", arg); + let test = arg.split("|"); + await launchConnection( + test[0], + parseInt(test[1]), + test[2], + parseInt(test[3]), + test[4] + ); + } +} + +var satisfied = false; +run(arguments).then(() => (satisfied = true)); +var mainThread = Cc["@mozilla.org/thread-manager;1"].getService().mainThread; +while (!satisfied) { + mainThread.processNextEvent(true); +} diff --git a/netwerk/test/unit/test_1073747.js b/netwerk/test/unit/test_1073747.js new file mode 100644 index 0000000000..b4df481330 --- /dev/null +++ b/netwerk/test/unit/test_1073747.js @@ -0,0 +1,42 @@ +// Test based on submitted one from Peter B Shalimoff + +"use strict"; + +var test = function(s, funcName) { + function Arg() {} + Arg.prototype.toString = function() { + info("Testing " + funcName + " with null args"); + return this.value; + }; + // create a generic arg lits of null, -1, and 10 nulls + var args = [s, -1]; + for (var i = 0; i < 10; ++i) { + args.push(new Arg()); + } + var up = Cc["@mozilla.org/network/url-parser;1?auth=maybe"].getService( + Ci.nsIURLParser + ); + try { + up[funcName].apply(up, args); + return args; + } catch (x) { + Assert.ok(true); // make sure it throws an exception instead of crashing + return x; + } +}; +var s = null; +var funcs = [ + "parseAuthority", + "parseFileName", + "parseFilePath", + "parsePath", + "parseServerInfo", + "parseURL", + "parseUserInfo", +]; + +function run_test() { + funcs.forEach(function(f) { + test(s, f); + }); +} diff --git a/netwerk/test/unit/test_304_responses.js b/netwerk/test/unit/test_304_responses.js new file mode 100644 index 0000000000..fee8a23fd5 --- /dev/null +++ b/netwerk/test/unit/test_304_responses.js @@ -0,0 +1,106 @@ +"use strict"; +// https://bugzilla.mozilla.org/show_bug.cgi?id=761228 + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; +const testFileName = "test_customConditionalRequest_304"; +const basePath = "/" + testFileName + "/"; + +XPCOMUtils.defineLazyGetter(this, "baseURI", function() { + return URL + basePath; +}); + +const unexpected304 = "unexpected304"; +const existingCached304 = "existingCached304"; + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function clearCache() { + var service = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService( + Ci.nsICacheStorageService + ); + service.clear(); +} + +function alwaysReturn304Handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + response.setHeader("Returned-From-Handler", "1"); +} + +function run_test() { + evict_cache_entries(); + + httpServer = new HttpServer(); + httpServer.registerPathHandler( + basePath + unexpected304, + alwaysReturn304Handler + ); + httpServer.registerPathHandler( + basePath + existingCached304, + alwaysReturn304Handler + ); + httpServer.start(-1); + run_next_test(); +} + +function finish_test(request, buffer) { + httpServer.stop(do_test_finished); +} + +function consume304(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 304); + Assert.equal(request.getResponseHeader("Returned-From-Handler"), "1"); + run_next_test(); +} + +// Test that we return a 304 response to the caller when we are not expecting +// a 304 response (i.e. when the server shouldn't have sent us one). +add_test(function test_unexpected_304() { + var chan = make_channel(baseURI + unexpected304); + chan.asyncOpen(new ChannelListener(consume304, null)); +}); + +// Test that we can cope with a 304 response that was (erroneously) stored in +// the cache. +add_test(function test_304_stored_in_cache() { + asyncOpenCacheEntry( + baseURI + existingCached304, + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(entryStatus, cacheEntry) { + cacheEntry.setMetaDataElement("request-method", "GET"); + cacheEntry.setMetaDataElement( + "response-head", + // eslint-disable-next-line no-useless-concat + "HTTP/1.1 304 Not Modified\r\n" + "\r\n" + ); + cacheEntry.metaDataReady(); + cacheEntry.close(); + + var chan = make_channel(baseURI + existingCached304); + + // make it a custom conditional request + chan.QueryInterface(Ci.nsIHttpChannel); + chan.setRequestHeader("If-None-Match", '"foo"', false); + + chan.asyncOpen(new ChannelListener(consume304, null)); + } + ); +}); diff --git a/netwerk/test/unit/test_307_redirect.js b/netwerk/test/unit/test_307_redirect.js new file mode 100644 index 0000000000..84295a071f --- /dev/null +++ b/netwerk/test/unit/test_307_redirect.js @@ -0,0 +1,96 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "uri", function() { + return URL + "/redirect"; +}); + +XPCOMUtils.defineLazyGetter(this, "noRedirectURI", function() { + return URL + "/content"; +}); + +var httpserver = null; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const requestBody = "request body"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", noRedirectURI, false); +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.writeFrom( + metadata.bodyInputStream, + metadata.bodyInputStream.available() + ); +} + +function noRedirectStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + var chan = make_channel(uri); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + uploadStream.setData(requestBody, requestBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "text/plain", -1); + chan.asyncOpen(new ChannelListener(noHeaderStreamObserver, null)); +} + +function noHeaderStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + var chan = make_channel(uri); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + var streamBody = + "Content-Type: text/plain\r\n" + + "Content-Length: " + + requestBody.length + + "\r\n\r\n" + + requestBody; + uploadStream.setData(streamBody, streamBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "", -1); + chan.asyncOpen(new ChannelListener(headerStreamObserver, null)); +} + +function headerStreamObserver(request, buffer) { + Assert.equal(buffer, requestBody); + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/redirect", redirectHandler); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setBoolPref("network.http.prompt-temp-redirect", false); + + var chan = make_channel(noRedirectURI); + var uploadStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + uploadStream.setData(requestBody, requestBody.length); + chan + .QueryInterface(Ci.nsIUploadChannel) + .setUploadStream(uploadStream, "text/plain", -1); + chan.asyncOpen(new ChannelListener(noRedirectStreamObserver, null)); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_421.js b/netwerk/test/unit/test_421.js new file mode 100644 index 0000000000..9ee0e1cb04 --- /dev/null +++ b/netwerk/test/unit/test_421.js @@ -0,0 +1,64 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/421"; +var httpbody = "0123456789"; +var channel; +var ios; + +function run_test() { + setup_test(); + do_test_pending(); +} + +function setup_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + channel = setupChannel(testpath); + + channel.asyncOpen(new ChannelListener(checkRequestResponse, channel)); +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +var iters = 0; + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + + if (!iters) { + response.setStatusLine("1.1", 421, "Not Authoritative " + iters); + } else { + response.setStatusLine("1.1", 200, "OK"); + } + ++iters; + + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function checkRequestResponse(request, data, context) { + Assert.equal(channel.responseStatus, 200); + Assert.equal(channel.responseStatusText, "OK"); + Assert.ok(channel.requestSucceeded); + + Assert.equal(channel.contentType, "text/plain"); + Assert.equal(channel.contentLength, httpbody.length); + Assert.equal(data, httpbody); + + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_MIME_params.js b/netwerk/test/unit/test_MIME_params.js new file mode 100644 index 0000000000..786d4fc48f --- /dev/null +++ b/netwerk/test/unit/test_MIME_params.js @@ -0,0 +1,784 @@ +/** + * Tests for parsing header fields using the syntax used in + * Content-Disposition and Content-Type + * + * See also https://bugzilla.mozilla.org/show_bug.cgi?id=609667 + */ + +"use strict"; + +var BS = "\\"; +var DQUOTE = '"'; + +// Test array: +// - element 0: "Content-Disposition" header to test +// under MIME (email): +// - element 1: correct value returned for disposition-type (empty param name) +// - element 2: correct value for filename returned +// under HTTP: +// (currently supports continuations; expected results without continuations +// are commented out for now) +// - element 3: correct value returned for disposition-type (empty param name) +// - element 4: correct value for filename returned +// +// 3 and 4 may be left out if they are identical + +var tests = [ + // No filename parameter: return nothing + ["attachment;", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // basic + ["attachment; filename=basic", "attachment", "basic"], + + // extended + ["attachment; filename*=UTF-8''extended", "attachment", "extended"], + + // prefer extended to basic (bug 588781) + [ + "attachment; filename=basic; filename*=UTF-8''extended", + "attachment", + "extended", + ], + + // prefer extended to basic (bug 588781) + [ + "attachment; filename*=UTF-8''extended; filename=basic", + "attachment", + "extended", + ], + + // use first basic value (invalid; error recovery) + ["attachment; filename=first; filename=wrong", "attachment", "first"], + + // old school bad HTTP servers: missing 'attachment' or 'inline' + // (invalid; error recovery) + ["filename=old", "filename=old", "old"], + + ["attachment; filename*=UTF-8''extended", "attachment", "extended"], + + // continuations not part of RFC 5987 (bug 610054) + [ + "attachment; filename*0=foo; filename*1=bar", + "attachment", + "foobar", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // Return first continuation (invalid; error recovery) + [ + "attachment; filename*0=first; filename*0=wrong; filename=basic", + "attachment", + "first", + /* "attachment", "basic" */ + ], + + // Only use correctly ordered continuations (invalid; error recovery) + [ + "attachment; filename*0=first; filename*1=second; filename*0=wrong", + "attachment", + "firstsecond", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // prefer continuation to basic (unless RFC 5987) + [ + "attachment; filename=basic; filename*0=foo; filename*1=bar", + "attachment", + "foobar", + /* "attachment", "basic" */ + ], + + // Prefer extended to basic and/or (broken or not) continuation + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0=first; filename*0=wrong; filename*=UTF-8''extended", + "attachment", + "extended", + ], + + // RFC 2231 not clear on correct outcome: we prefer non-continued extended + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*=UTF-8''extended; filename*0=foo; filename*1=bar", + "attachment", + "extended", + ], + + // Gaps should result in returning only value until gap hit + // (invalid; error recovery) + [ + "attachment; filename*0=foo; filename*2=bar", + "attachment", + "foo", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // Don't allow leading 0's (*01) (invalid; error recovery) + [ + "attachment; filename*0=foo; filename*01=bar", + "attachment", + "foo", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // continuations should prevail over non-extended (unless RFC 5987) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*2*=%20extended", + "attachment", + "multiline extended", + /* "attachment", "basic" */ + ], + + // Gaps should result in returning only value until gap hit + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*3*=%20extended", + "attachment", + "multiline", + /* "attachment", "basic" */ + ], + + // First series, only please, and don't slurp up higher elements (*2 in this + // case) from later series into earlier one (invalid; error recovery) + [ + "attachment; filename=basic; filename*0*=UTF-8''multi;\r\n" + + " filename*1=line;\r\n" + + " filename*0*=UTF-8''wrong;\r\n" + + " filename*1=bad;\r\n" + + " filename*2=evil", + "attachment", + "multiline", + /* "attachment", "basic" */ + ], + + // RFC 2231 not clear on correct outcome: we prefer non-continued extended + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*0=UTF-8''multi\r\n;" + + " filename*=UTF-8''extended;\r\n" + + " filename*1=line;\r\n" + + " filename*2*=%20extended", + "attachment", + "extended", + ], + + // sneaky: if unescaped, make sure we leave UTF-8'' in value + [ + "attachment; filename*0=UTF-8''unescaped;\r\n" + + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", + "attachment", + "UTF-8''unescaped so includes UTF-8'' in value", + /* "attachment", Cr.NS_ERROR_INVALID_ARG */ + ], + + // sneaky: if unescaped, make sure we leave UTF-8'' in value + [ + "attachment; filename=basic; filename*0=UTF-8''unescaped;\r\n" + + " filename*1*=%20so%20includes%20UTF-8''%20in%20value", + "attachment", + "UTF-8''unescaped so includes UTF-8'' in value", + /* "attachment", "basic" */ + ], + + // Prefer basic over invalid continuation + // (invalid; error recovery) + [ + "attachment; filename=basic; filename*1=multi;\r\n" + + " filename*2=line;\r\n" + + " filename*3*=%20extended", + "attachment", + "basic", + ], + + // support digits over 10 + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*13=d;filename*14=e;filename*15=f\r\n", + "attachment", + "0123456789abcdef", + /* "attachment", "basic" */ + ], + + // support digits over 10 (detect gaps) + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*14=e\r\n", + "attachment", + "0123456789abc", + /* "attachment", "basic" */ + ], + + // return nothing: invalid + // (invalid; error recovery) + [ + "attachment; filename*1=multi;\r\n" + + " filename*2=line;\r\n" + + " filename*3*=%20extended", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // Bug 272541: Empty disposition type treated as "attachment" + + // sanity check + [ + "attachment; filename=foo.html", + "attachment", + "foo.html", + "attachment", + "foo.html", + ], + + // the actual bug + [ + "; filename=foo.html", + Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY, + "foo.html", + Cr.NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY, + "foo.html", + ], + + // regression check, but see bug 671204 + [ + "filename=foo.html", + "filename=foo.html", + "foo.html", + "filename=foo.html", + "foo.html", + ], + + // Bug 384571: RFC 2231 parameters not decoded when appearing in reversed order + + // check ordering + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1=1; filename*2=2;filename*3=3;filename*4=4;filename*5=5;\r\n" + + " filename*6=6; filename*7=7;filename*8=8;filename*9=9;filename*10=a;\r\n" + + " filename*11=b; filename*12=c;filename*13=d;filename*15=f;filename*14=e;\r\n", + "attachment", + "0123456789abcdef", + /* "attachment", "basic" */ + ], + + // check non-digits in sequence numbers + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*1a=1\r\n", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check duplicate sequence numbers + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*0=bad; filename*1=1;\r\n", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check overflow + [ + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + + " filename*11111111111111111111111111111111111111111111111111111111111=1", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check underflow + [ + // eslint-disable-next-line no-useless-concat + "attachment; filename=basic; filename*0*=UTF-8''0;\r\n" + " filename*-1=1", + "attachment", + "0", + /* "attachment", "basic" */ + ], + + // check mixed token/quoted-string + [ + 'attachment; filename=basic; filename*0="0";\r\n' + + " filename*1=1;\r\n" + + " filename*2*=%32", + "attachment", + "012", + /* "attachment", "basic" */ + ], + + // check empty sequence number + [ + "attachment; filename=basic; filename**=UTF-8''0\r\n", + "attachment", + "basic", + "attachment", + "basic", + ], + + // Bug 419157: ensure that a MIME parameter with no charset information + // fallbacks to Latin-1 + + [ + "attachment;filename=IT839\x04\xB5(m8)2.pdf;", + "attachment", + "IT839\u0004\u00b5(m8)2.pdf", + ], + + // Bug 588389: unescaping backslashes in quoted string parameters + + // '\"', should be parsed as '"' + [ + "attachment; filename=" + DQUOTE + (BS + DQUOTE) + DQUOTE, + "attachment", + DQUOTE, + ], + + // 'a\"b', should be parsed as 'a"b' + [ + "attachment; filename=" + DQUOTE + "a" + (BS + DQUOTE) + "b" + DQUOTE, + "attachment", + "a" + DQUOTE + "b", + ], + + // '\x', should be parsed as 'x' + ["attachment; filename=" + DQUOTE + (BS + "x") + DQUOTE, "attachment", "x"], + + // test empty param (quoted-string) + ["attachment; filename=" + DQUOTE + DQUOTE, "attachment", ""], + + // test empty param + ["attachment; filename=", "attachment", ""], + + // Bug 601933: RFC 2047 does not apply to parameters (at least in HTTP) + [ + "attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=", + "attachment", + "foo-\u00e4.html", + /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */ + ], + + [ + 'attachment; filename="=?ISO-8859-1?Q?foo-=E4.html?="', + "attachment", + "foo-\u00e4.html", + /* "attachment", "=?ISO-8859-1?Q?foo-=E4.html?=" */ + ], + + // format sent by GMail as of 2012-07-23 (5987 overrides 2047) + [ + "attachment; filename=\"=?ISO-8859-1?Q?foo-=E4.html?=\"; filename*=UTF-8''5987", + "attachment", + "5987", + ], + + // Bug 651185: double quotes around 2231/5987 encoded param + // Change reverted to backwards compat issues with various web services, + // such as OWA (Bug 703015), plus similar problems in Thunderbird. If this + // is tried again in the future, email probably needs to be special-cased. + + // sanity check + ["attachment; filename*=utf-8''%41", "attachment", "A"], + + // the actual bug + [ + "attachment; filename*=" + DQUOTE + "utf-8''%41" + DQUOTE, + "attachment", + "A", + ], + // previously with the fix for 651185: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 670333: Content-Disposition parser does not require presence of "=" + // in params + + // sanity check + ["attachment; filename*=UTF-8''foo-%41.html", "attachment", "foo-A.html"], + + // the actual bug + [ + "attachment; filename *=UTF-8''foo-%41.html", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // the actual bug, without 2231/5987 encoding + ["attachment; filename X", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // sanity check with WS on both sides + ["attachment; filename = foo-A.html", "attachment", "foo-A.html"], + + // Bug 685192: in RFC2231/5987 encoding, a missing charset field should be + // treated as error + + // the actual bug + ["attachment; filename*=''foo", "attachment", "foo"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // sanity check + ["attachment; filename*=a''foo", "attachment", "foo"], + + // Bug 692574: RFC2231/5987 decoding should not tolerate missing single + // quotes + + // one missing + ["attachment; filename*=UTF-8'foo-%41.html", "attachment", "foo-A.html"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // both missing + ["attachment; filename*=foo-%41.html", "attachment", "foo-A.html"], + // previously with the fix for 692574: + // "attachment", Cr.NS_ERROR_INVALID_ARG], + + // make sure fallback works + [ + "attachment; filename*=UTF-8'foo-%41.html; filename=bar.html", + "attachment", + "foo-A.html", + ], + // previously with the fix for 692574: + // "attachment", "bar.html"], + + // Bug 693806: RFC2231/5987 encoding: charset information should be treated + // as authoritative + + // UTF-8 labeled ISO-8859-1 + ["attachment; filename*=ISO-8859-1''%c3%a4", "attachment", "\u00c3\u00a4"], + + // UTF-8 labeled ISO-8859-1, but with octets not allowed in ISO-8859-1 + // accepts x82, understands it as Win1252, maps it to Unicode \u20a1 + [ + "attachment; filename*=ISO-8859-1''%e2%82%ac", + "attachment", + "\u00e2\u201a\u00ac", + ], + + // defective UTF-8 + ["attachment; filename*=UTF-8''A%e4B", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // defective UTF-8, with fallback + [ + "attachment; filename*=UTF-8''A%e4B; filename=fallback", + "attachment", + "fallback", + ], + + // defective UTF-8 (continuations), with fallback + [ + "attachment; filename*0*=UTF-8''A%e4B; filename=fallback", + "attachment", + "fallback", + ], + + // check that charsets aren't mixed up + [ + "attachment; filename*0*=ISO-8859-15''euro-sign%3d%a4; filename*=ISO-8859-1''currency-sign%3d%a4", + "attachment", + "currency-sign=\u00a4", + ], + + // same as above, except reversed + [ + "attachment; filename*=ISO-8859-1''currency-sign%3d%a4; filename*0*=ISO-8859-15''euro-sign%3d%a4", + "attachment", + "currency-sign=\u00a4", + ], + + // Bug 704989: add workaround for broken Outlook Web App (OWA) + // attachment handling + + ['attachment; filename*="a%20b"', "attachment", "a b"], + + // Bug 717121: crash nsMIMEHeaderParamImpl::DoParameterInternal + + ['attachment; filename="', "attachment", ""], + + // We used to read past string if last param w/o = and ; + // Note: was only detected on windows PGO builds + ["attachment; filename=foo; trouble", "attachment", "foo"], + + // Same, followed by space, hits another case + ["attachment; filename=foo; trouble ", "attachment", "foo"], + + ["attachment", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 730574: quoted-string in RFC2231-continuations not handled + + [ + 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\r.html"', + "attachment", + "foobar.html", + /* "attachment", "basic" */ + ], + + // unmatched escape char + [ + 'attachment; filename=basic; filename*0="foo"; filename*1="\\b\\a\\', + "attachment", + "fooba\\", + /* "attachment", "basic" */ + ], + + // Bug 732369: Content-Disposition parser does not require presence of ";" between params + // optimally, this would not even return the disposition type "attachment" + + [ + "attachment; extension=bla filename=foo", + "attachment", + Cr.NS_ERROR_INVALID_ARG, + ], + + // Bug 1440677 - spaces inside filenames ought to be quoted, but too many + // servers do the wrong thing and most browsers accept this, so we were + // forced to do the same for compat. + ["attachment; filename=foo extension=bla", "attachment", "foo extension=bla"], + + ["attachment filename=foo", "attachment", Cr.NS_ERROR_INVALID_ARG], + + // Bug 777687: handling of broken %escapes + + ["attachment; filename*=UTF-8''f%oo; filename=bar", "attachment", "bar"], + + ["attachment; filename*=UTF-8''foo%; filename=bar", "attachment", "bar"], + + // Bug 783502 - xpcshell test netwerk/test/unit/test_MIME_params.js fails on AddressSanitizer + ['attachment; filename="\\b\\a\\', "attachment", "ba\\"], + + // Bug 1412213 - do continue to parse, behind an empty parameter + ["attachment; ; filename=foo", "attachment", "foo"], + + // Bug 1412213 - do continue to parse, behind a parameter w/o = + ["attachment; badparameter; filename=foo", "attachment", "foo"], + + // Bug 1440677 - spaces inside filenames ought to be quoted, but too many + // servers do the wrong thing and most browsers accept this, so we were + // forced to do the same for compat. + ["attachment; filename=foo bar.html", "attachment", "foo bar.html"], + // Note: we keep the tab character, but later validation will replace with a space, + // as file systems do not like tab characters. + ["attachment; filename=foo\tbar.html", "attachment", "foo\tbar.html"], + // Newlines get stripped completely (in practice, http header parsing may + // munge these into spaces before they get to us, but we should check we deal + // with them either way): + ["attachment; filename=foo\nbar.html", "attachment", "foobar.html"], + ["attachment; filename=foo\r\nbar.html", "attachment", "foobar.html"], + ["attachment; filename=foo\rbar.html", "attachment", "foobar.html"], + + // Trailing rubbish shouldn't matter: + ["attachment; filename=foo bar; garbage", "attachment", "foo bar"], + ["attachment; filename=foo bar; extension=blah", "attachment", "foo bar"], + + // Check that whitespace processing can't crash. + ["attachment; filename = ", "attachment", ""], +]; + +var rfc5987paramtests = [ + [ + // basic test + "UTF-8'language'value", + "value", + "language", + Cr.NS_OK, + ], + [ + // percent decoding + "UTF-8''1%202", + "1 2", + "", + Cr.NS_OK, + ], + [ + // UTF-8 + "UTF-8''%c2%a3%20and%20%e2%82%ac%20rates", + "\u00a3 and \u20ac rates", + "", + Cr.NS_OK, + ], + [ + // missing charset + "''abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // ISO-8859-1: unsupported + "ISO-8859-1''%A3%20rates", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // unknown charset + "foo''abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // missing component + "abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // missing component + "'abc", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // illegal chars + "UTF-8''a b", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%zz", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%b", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken % escapes + "UTF-8''a%", + "", + "", + Cr.NS_ERROR_INVALID_ARG, + ], + [ + // broken UTF-8 + "UTF-8''%A3%20rates", + "", + "", + 0x8050000e /* NS_ERROR_UDEC_ILLEGALINPUT */, + ], +]; + +function do_tests(whichRFC) { + var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService( + Ci.nsIMIMEHeaderParam + ); + + var unused = { value: null }; + + for (var i = 0; i < tests.length; ++i) { + dump("Testing #" + i + ": " + tests[i] + "\n"); + + // check disposition type + var expectedDt = + tests[i].length == 3 || whichRFC == 0 ? tests[i][1] : tests[i][3]; + + try { + var result; + + if (whichRFC == 0) { + result = mhp.getParameter(tests[i][0], "", "UTF-8", true, unused); + } else { + result = mhp.getParameterHTTP(tests[i][0], "", "UTF-8", true, unused); + } + + Assert.equal(result, expectedDt); + } catch (e) { + // Tests can also succeed by expecting to fail with given error code + if (e.result) { + // Allow following tests to run by catching exception from do_check_eq() + try { + Assert.equal(e.result, expectedDt); + } catch (e) {} + } + continue; + } + + // check filename parameter + var expectedFn = + tests[i].length == 3 || whichRFC == 0 ? tests[i][2] : tests[i][4]; + + try { + var result; + + if (whichRFC == 0) { + result = mhp.getParameter( + tests[i][0], + "filename", + "UTF-8", + true, + unused + ); + } else { + result = mhp.getParameterHTTP( + tests[i][0], + "filename", + "UTF-8", + true, + unused + ); + } + + Assert.equal(result, expectedFn); + } catch (e) { + // Tests can also succeed by expecting to fail with given error code + if (e.result) { + // Allow following tests to run by catching exception from do_check_eq() + try { + Assert.equal(e.result, expectedFn); + } catch (e) {} + } + continue; + } + } +} + +function test_decode5987Param() { + var mhp = Cc["@mozilla.org/network/mime-hdrparam;1"].getService( + Ci.nsIMIMEHeaderParam + ); + + for (var i = 0; i < rfc5987paramtests.length; ++i) { + dump("Testing #" + i + ": " + rfc5987paramtests[i] + "\n"); + + var lang = {}; + try { + var decoded = mhp.decodeRFC5987Param(rfc5987paramtests[i][0], lang); + if (rfc5987paramtests[i][3] == Cr.NS_OK) { + Assert.equal(rfc5987paramtests[i][1], decoded); + Assert.equal(rfc5987paramtests[i][2], lang.value); + } else { + Assert.equal(rfc5987paramtests[i][3], "instead got: " + decoded); + } + } catch (e) { + Assert.equal(rfc5987paramtests[i][3], e.result); + } + } +} + +function run_test() { + // Test RFC 2231 (complete header field values) + do_tests(0); + + // Test RFC 5987 (complete header field values) + do_tests(1); + + // tests for RFC5987 parameter parsing + test_decode5987Param(); +} diff --git a/netwerk/test/unit/test_NetUtil.js b/netwerk/test/unit/test_NetUtil.js new file mode 100644 index 0000000000..9670b3ba19 --- /dev/null +++ b/netwerk/test/unit/test_NetUtil.js @@ -0,0 +1,818 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file tests the methods on NetUtil.jsm. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// We need the profile directory so the test harness will clean up our test +// files. +do_get_profile(); + +const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1"; +const SAFE_OUTPUT_STREAM_CONTRACT_ID = + "@mozilla.org/network/safe-file-output-stream;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) { + "use strict"; + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(aFile, -1, 0, 0); + + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(fstream, "UTF-8", 0, 0); + + let string = {}; + cstream.readString(-1, string); + cstream.close(); + return string.value; +} + +/** + * Tests asynchronously writing a file using NetUtil.asyncCopy. + * + * @param aContractId + * The contract ID to use for the output stream + * @param aDeferOpen + * Whether to use DEFER_OPEN in the output stream. + */ +function async_write_file(aContractId, aDeferOpen) { + do_test_pending(); + + // First, we need an output file to write to. + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("NetUtil-async-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // Then, we need an output stream to our output file. + let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + + // Finally, we need an input stream to take data from. + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + NetUtil.asyncCopy(istream, ostream, function(aResult) { + // Make sure the copy was successful! + Assert.ok(Components.isSuccessCode(aResult)); + + // Check the file contents. + Assert.equal(TEST_DATA, getFileContents(file)); + + // Finish the test. + do_test_finished(); + run_next_test(); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +// Test NetUtil.asyncCopy for all possible buffering scenarios +function test_async_copy() { + // Create a data sample + function make_sample(text) { + let data = []; + for (let i = 0; i <= 100; ++i) { + data.push(text); + } + return data.join(); + } + + // Create an input buffer holding some data + function make_input(isBuffered, data) { + if (isBuffered) { + // String input streams are buffered + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(data, data.length); + return istream; + } + + // File input streams are not buffered, so let's create a file + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, -1, 0); + ostream.write(data, data.length); + ostream.close(); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + istream.init(file, -1, 0, 0); + + return istream; + } + + // Create an output buffer holding some data + function make_output(isBuffered) { + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, -1, 0); + + if (!isBuffered) { + return { file, sink: ostream }; + } + + let bstream = Cc[ + "@mozilla.org/network/buffered-output-stream;1" + ].createInstance(Ci.nsIBufferedOutputStream); + bstream.init(ostream, 256); + return { file, sink: bstream }; + } + (async function() { + do_test_pending(); + for (let bufferedInput of [true, false]) { + for (let bufferedOutput of [true, false]) { + let text = + "test_async_copy with " + + (bufferedInput ? "buffered input" : "unbuffered input") + + ", " + + (bufferedOutput ? "buffered output" : "unbuffered output"); + info(text); + let TEST_DATA = "[" + make_sample(text) + "]"; + let source = make_input(bufferedInput, TEST_DATA); + let { file, sink } = make_output(bufferedOutput); + let result = await new Promise(resolve => { + NetUtil.asyncCopy(source, sink, resolve); + }); + + // Make sure the copy was successful! + if (!Components.isSuccessCode(result)) { + do_throw(new Components.Exception("asyncCopy error", result)); + } + + // Check the file contents. + Assert.equal(TEST_DATA, getFileContents(file)); + } + } + + do_test_finished(); + run_next_test(); + })(); +} + +function test_async_write_file() { + async_write_file(OUTPUT_STREAM_CONTRACT_ID); +} + +function test_async_write_file_deferred() { + async_write_file(OUTPUT_STREAM_CONTRACT_ID, true); +} + +function test_async_write_file_safe() { + async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID); +} + +function test_async_write_file_safe_deferred() { + async_write_file(SAFE_OUTPUT_STREAM_CONTRACT_ID, true); +} + +function test_newURI_no_spec_throws() { + try { + NetUtil.newURI(); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_newURI() { + let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + // Check that we get the same URI back from the IO service and the utility + // method. + const TEST_URI = "http://mozilla.org"; + let iosURI = ios.newURI(TEST_URI); + let NetUtilURI = NetUtil.newURI(TEST_URI); + Assert.ok(iosURI.equals(NetUtilURI)); + + run_next_test(); +} + +function test_newURI_takes_nsIFile() { + let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + // Create a test file that we can pass into NetUtil.newURI + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("NetUtil-test-file.tmp"); + + // Check that we get the same URI back from the IO service and the utility + // method. + let iosURI = ios.newFileURI(file); + let NetUtilURI = NetUtil.newURI(file); + Assert.ok(iosURI.equals(NetUtilURI)); + + run_next_test(); +} + +function test_asyncFetch_no_channel() { + try { + NetUtil.asyncFetch(null, function() {}); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_asyncFetch_no_callback() { + try { + NetUtil.asyncFetch({}); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_asyncFetch_with_nsIChannel() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Create our channel. + let channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/test", + loadUsingSystemPrincipal: true, + }); + + // Open our channel asynchronously. + NetUtil.asyncFetch(channel, function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }); +} + +function test_asyncFetch_with_nsIURI() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Create our URI. + let uri = NetUtil.newURI( + "http://localhost:" + server.identity.primaryPort + "/test" + ); + + // Open our URI asynchronously. + NetUtil.asyncFetch( + { + uri, + loadUsingSystemPrincipal: true, + }, + function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_string() { + const TEST_DATA = "this is a test string"; + + // Start the http server, and register our handler. + let server = new HttpServer(); + server.registerPathHandler("/test", function(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.write(TEST_DATA); + }); + server.start(-1); + + // Open our location asynchronously. + NetUtil.asyncFetch( + { + uri: "http://localhost:" + server.identity.primaryPort + "/test", + loadUsingSystemPrincipal: true, + }, + function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + server.stop(run_next_test); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_nsIFile() { + const TEST_DATA = "this is a test string"; + + // First we need a file to read from. + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("NetUtil-asyncFetch-test-file.tmp"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + // Write the test data to the file. + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, -1, -1, 0); + ostream.write(TEST_DATA, TEST_DATA.length); + + // Sanity check to make sure the data was written. + Assert.equal(TEST_DATA, getFileContents(file)); + + // Open our file asynchronously. + // Note that this causes main-tread I/O and should be avoided in production. + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true, + }, + function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + let result = is.read(TEST_DATA.length); + Assert.equal(TEST_DATA, result); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_with_nsIInputString() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + // Read the input stream asynchronously. + NetUtil.asyncFetch( + istream, + function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that we got the right data. + Assert.equal(aInputStream.available(), TEST_DATA.length); + Assert.equal( + NetUtil.readInputStreamToString(aInputStream, TEST_DATA.length), + TEST_DATA + ); + + run_next_test(); + }, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); +} + +function test_asyncFetch_does_not_block() { + // Create our channel that has no data. + let channel = NetUtil.newChannel({ + uri: "data:text/plain,", + loadUsingSystemPrincipal: true, + }); + + // Open our channel asynchronously. + NetUtil.asyncFetch(channel, function(aInputStream, aResult) { + // Check that we had success. + Assert.ok(Components.isSuccessCode(aResult)); + + // Check that reading a byte throws that the stream was closed (as opposed + // saying it would block). + let is = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + is.init(aInputStream); + try { + is.read(1); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_BASE_STREAM_CLOSED); + } + + run_next_test(); + }); +} + +function test_newChannel_no_specifier() { + try { + NetUtil.newChannel(); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_newChannel_with_string() { + const TEST_SPEC = "http://mozilla.org"; + + // Check that we get the same URI back from channel the IO service creates and + // the channel the utility method creates. + let ios = Services.io; + let iosChannel = ios.newChannel( + TEST_SPEC, + null, + null, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let NetUtilChannel = NetUtil.newChannel({ + uri: TEST_SPEC, + loadUsingSystemPrincipal: true, + }); + Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI)); + + run_next_test(); +} + +function test_newChannel_with_nsIURI() { + const TEST_SPEC = "http://mozilla.org"; + + // Check that we get the same URI back from channel the IO service creates and + // the channel the utility method creates. + let uri = NetUtil.newURI(TEST_SPEC); + let iosChannel = Services.io.newChannelFromURI( + uri, + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let NetUtilChannel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + Assert.ok(iosChannel.URI.equals(NetUtilChannel.URI)); + + run_next_test(); +} + +function test_newChannel_with_options() { + let uri = "data:text/plain,"; + + let iosChannel = Services.io.newChannelFromURI( + NetUtil.newURI(uri), + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + function checkEqualToIOSChannel(channel) { + Assert.ok(iosChannel.URI.equals(channel.URI)); + } + + checkEqualToIOSChannel( + NetUtil.newChannel({ + uri, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }) + ); + + checkEqualToIOSChannel( + NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }) + ); + + run_next_test(); +} + +function test_newChannel_with_wrong_options() { + let uri = "data:text/plain,"; + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + + Assert.throws(() => { + NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }, null, null); + }, /requires a single object argument/); + + Assert.throws(() => { + NetUtil.newChannel({ loadUsingSystemPrincipal: true }); + }, /requires the 'uri' property/); + + Assert.throws(() => { + NetUtil.newChannel({ uri, loadingNode: true }); + }, /requires the 'securityFlags'/); + + Assert.throws(() => { + NetUtil.newChannel({ uri, securityFlags: 0 }); + }, /requires at least one of the 'loadingNode'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadingPrincipal: systemPrincipal, + securityFlags: 0, + }); + }, /requires the 'contentPolicyType'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: systemPrincipal, + }); + }, /to be 'true' or 'undefined'/); + + Assert.throws(() => { + NetUtil.newChannel({ + uri, + loadingPrincipal: systemPrincipal, + loadUsingSystemPrincipal: true, + }); + }, /does not accept 'loadUsingSystemPrincipal'/); + + run_next_test(); +} + +function test_readInputStreamToString() { + const TEST_DATA = "this is a test string\0 with an embedded null"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsISupportsCString + ); + istream.data = TEST_DATA; + + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA.length), + TEST_DATA + ); + + run_next_test(); +} + +function test_readInputStreamToString_no_input_stream() { + try { + NetUtil.readInputStreamToString("hi", 2); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_readInputStreamToString_no_bytes_arg() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + try { + NetUtil.readInputStreamToString(istream); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INVALID_ARG); + } + + run_next_test(); +} + +function test_readInputStreamToString_blocking_stream() { + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0, null); + + try { + NetUtil.readInputStreamToString(pipe.inputStream, 10); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK); + } + run_next_test(); +} + +function test_readInputStreamToString_too_many_bytes() { + const TEST_DATA = "this is a test string"; + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + istream.setData(TEST_DATA, TEST_DATA.length); + + try { + NetUtil.readInputStreamToString(istream, TEST_DATA.length + 10); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FAILURE); + } + + run_next_test(); +} + +function test_readInputStreamToString_with_charset() { + const TEST_DATA = "\uff10\uff11\uff12\uff13"; + const TEST_DATA_UTF8 = "\xef\xbc\x90\xef\xbc\x91\xef\xbc\x92\xef\xbc\x93"; + const TEST_DATA_SJIS = "\x82\x4f\x82\x50\x82\x51\x82\x52"; + + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + }), + TEST_DATA + ); + + istream.setData(TEST_DATA_SJIS, TEST_DATA_SJIS.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_SJIS.length, { + charset: "Shift_JIS", + }), + TEST_DATA + ); + + run_next_test(); +} + +function test_readInputStreamToString_invalid_sequence() { + const TEST_DATA = "\ufffd\ufffd\ufffd\ufffd"; + const TEST_DATA_UTF8 = "\xaa\xaa\xaa\xaa"; + + let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + try { + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + }); + do_throw("should throw!"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_INPUT); + } + + istream.setData(TEST_DATA_UTF8, TEST_DATA_UTF8.length); + Assert.equal( + NetUtil.readInputStreamToString(istream, TEST_DATA_UTF8.length, { + charset: "UTF-8", + replacement: Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER, + }), + TEST_DATA + ); + + run_next_test(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Runner + +[ + test_async_copy, + test_async_write_file, + test_async_write_file_deferred, + test_async_write_file_safe, + test_async_write_file_safe_deferred, + test_newURI_no_spec_throws, + test_newURI, + test_newURI_takes_nsIFile, + test_asyncFetch_no_channel, + test_asyncFetch_no_callback, + test_asyncFetch_with_nsIChannel, + test_asyncFetch_with_nsIURI, + test_asyncFetch_with_string, + test_asyncFetch_with_nsIFile, + test_asyncFetch_with_nsIInputString, + test_asyncFetch_does_not_block, + test_newChannel_no_specifier, + test_newChannel_with_string, + test_newChannel_with_nsIURI, + test_newChannel_with_options, + test_newChannel_with_wrong_options, + test_readInputStreamToString, + test_readInputStreamToString_no_input_stream, + test_readInputStreamToString_no_bytes_arg, + test_readInputStreamToString_blocking_stream, + test_readInputStreamToString_too_many_bytes, + test_readInputStreamToString_with_charset, + test_readInputStreamToString_invalid_sequence, +].forEach(f => add_test(f)); +var index = 0; diff --git a/netwerk/test/unit/test_SuperfluousAuth.js b/netwerk/test/unit/test_SuperfluousAuth.js new file mode 100644 index 0000000000..fa50e385cd --- /dev/null +++ b/netwerk/test/unit/test_SuperfluousAuth.js @@ -0,0 +1,99 @@ +/* + +Create two http requests with the same URL in which has a user name. We allow +first http request to be loaded and saved in the cache, so the second request +will be served from the cache. However, we disallow loading by returning 1 +in the prompt service. In the end, the second request will be failed. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://foo@localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +const gMockPromptService = { + firstTimeCalled: false, + confirmExBC() { + if (!this.firstTimeCalled) { + this.firstTimeCalled = true; + return 0; + } + + return 1; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), +}; + +var gMockPromptServiceCID = MockRegistrar.register( + "@mozilla.org/embedcomp/prompt-service;1", + gMockPromptService +); + +registerCleanupFunction(() => { + MockRegistrar.unregister(gMockPromptServiceCID); +}); + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +const responseBody = "body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Content-Length", "" + responseBody.length); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = makeChan(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = makeChan(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + Assert.ok(gMockPromptService.firstTimeCalled, "Prompt service invoked"); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(request.status, Cr.NS_ERROR_ABORT); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_URIs.js b/netwerk/test/unit/test_URIs.js new file mode 100644 index 0000000000..df55ebe157 --- /dev/null +++ b/netwerk/test/unit/test_URIs.js @@ -0,0 +1,1042 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var gIoService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests +// or: cd objdir; make SOLO_FILE="test_URIs.js" -C netwerk/test/ check-one + +// See also test_URIs2.js. + +// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code) +// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 +// http://greenbytes.de/tech/tc/uris/ + +// TEST DATA +// --------- +var gTests = [ + { + spec: "about:blank", + scheme: "about", + prePath: "about:", + pathQueryRef: "blank", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "about:foobar", + scheme: "about", + prePath: "about:", + pathQueryRef: "foobar", + ref: "", + nsIURL: false, + nsINestedURI: false, + immutable: true, + }, + { + spec: "chrome://foobar/somedir/somefile.xml", + scheme: "chrome", + prePath: "chrome://foobar", + pathQueryRef: "/somedir/somefile.xml", + ref: "", + nsIURL: true, + nsINestedURI: false, + immutable: true, + }, + { + spec: "data:text/html;charset=utf-8,", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/html;charset=utf-8,", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/html;charset=utf-8,\r\n\t", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/html;charset=utf-8,", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/plain,hello%20world", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,hello%20world", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "data:text/plain,hello world", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,hello world", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "file:///dir/afile", + scheme: "data", + prePath: "data:", + pathQueryRef: "text/plain,2", + ref: "", + relativeURI: "data:te\nxt/plain,2", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "file://", + scheme: "file", + prePath: "file://", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///", + scheme: "file", + prePath: "file://", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///myFile.html", + scheme: "file", + prePath: "file://", + pathQueryRef: "/myFile.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///dir/afile", + scheme: "file", + prePath: "file://", + pathQueryRef: "/dir/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "file:///dir/dir2/", + scheme: "file", + prePath: "file://", + pathQueryRef: "/dir/dir2/data/text/plain,2", + ref: "", + relativeURI: "data/text/plain,2", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://ftp.mozilla.org/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://ftp.mozilla.org", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://foo:bar@ftp.mozilla.org:100/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://foo:bar@ftp.mozilla.org:100", + port: 100, + username: "foo", + password: "bar", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "ftp://foo:@ftp.mozilla.org:100/pub/mozilla.org/README", + scheme: "ftp", + prePath: "ftp://foo@ftp.mozilla.org:100", + port: 100, + username: "foo", + password: "", + pathQueryRef: "/pub/mozilla.org/README", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + //Bug 706249 + { + spec: "gopher://mozilla.org/", + scheme: "gopher", + prePath: "gopher:", + pathQueryRef: "//mozilla.org/", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://www.example.com/", + scheme: "http", + prePath: "http://www.example.com", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://www.exa\nmple.com/", + scheme: "http", + prePath: "http://www.example.com", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://10.32.4.239/", + scheme: "http", + prePath: "http://10.32.4.239", + host: "10.32.4.239", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://[::192.9.5.5]/ipng", + scheme: "http", + prePath: "http://[::c009:505]", + host: "::c009:505", + pathQueryRef: "/ipng", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8888/index.html", + scheme: "http", + prePath: "http://[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:8888", + host: "fedc:ba98:7654:3210:fedc:ba98:7654:3210", + port: 8888, + pathQueryRef: "/index.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://bar:foo@www.mozilla.org:8080/pub/mozilla.org/README.html", + scheme: "http", + prePath: "http://bar:foo@www.mozilla.org:8080", + port: 8080, + username: "bar", + password: "foo", + host: "www.mozilla.org", + pathQueryRef: "/pub/mozilla.org/README.html", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "jar:resource://!/", + scheme: "jar", + prePath: "jar:", + pathQueryRef: "resource:///!/", + ref: "", + nsIURL: true, + nsINestedURI: true, + }, + { + spec: "jar:resource://gre/chrome.toolkit.jar!/", + scheme: "jar", + prePath: "jar:", + pathQueryRef: "resource://gre/chrome.toolkit.jar!/", + ref: "", + nsIURL: true, + nsINestedURI: true, + }, + { + spec: "mailto:webmaster@mozilla.com", + scheme: "mailto", + prePath: "mailto:", + pathQueryRef: "webmaster@mozilla.com", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "javascript:new Date()", + scheme: "javascript", + prePath: "javascript:", + pathQueryRef: "new Date()", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "blob:123456", + scheme: "blob", + prePath: "blob:", + pathQueryRef: "123456", + ref: "", + nsIURL: false, + nsINestedURI: false, + immutable: true, + }, + { + spec: "place:sort=8&maxResults=10", + scheme: "place", + prePath: "place:", + pathQueryRef: "sort=8&maxResults=10", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "resource://gre/", + scheme: "resource", + prePath: "resource://gre", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "resource://gre/components/", + scheme: "resource", + prePath: "resource://gre", + pathQueryRef: "/components/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // Adding more? Consider adding to test_URIs2.js instead, so that neither + // test runs for *too* long, risking timeouts on slow platforms. +]; + +var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"]; + +// TEST HELPER FUNCTIONS +// --------------------- +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +// Checks that the URIs satisfy equals(), in both possible orderings. +// Also checks URI.equalsExceptRef(), because equal URIs should also be equal +// when we ignore the ref. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"); + aCheckTrueFunc(aURI1.equals(aURI2)); + do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"); + aCheckTrueFunc(aURI2.equals(aURI1)); + + // (Only take the extra step of testing 'equalsExceptRef' when we expect the + // URIs to really be equal. In 'todo' cases, the URIs may or may not be + // equal when refs are ignored - there's no way of knowing in general.) + if (aCheckTrueFunc == ok) { + do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc); + } +} + +// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info( + "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')" + ); + aCheckTrueFunc(aURI1.equalsExceptRef(aURI2)); + do_info( + "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')" + ); + aCheckTrueFunc(aURI2.equalsExceptRef(aURI1)); +} + +// Checks that the given property on aURI matches the corresponding property +// in the test bundle (or matches some function of that corresponding property, +// if aTestFunctor is passed in). +function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) { + if (aTest[aPropertyName]) { + var expectedVal = aTestFunctor + ? aTestFunctor(aTest[aPropertyName]) + : aTest[aPropertyName]; + + do_info( + "testing " + + aPropertyName + + " of " + + (aTestFunctor ? "modified '" : "'") + + aTest.spec + + "' is '" + + expectedVal + + "'" + ); + Assert.equal(aURI[aPropertyName], expectedVal); + } +} + +// Test that a given URI parses correctly into its various components. +function do_test_uri_basic(aTest) { + var URI; + + do_info( + "Basic tests for " + + aTest.spec + + " relative URI: " + + (aTest.relativeURI === undefined ? "(none)" : aTest.relativeURI) + ); + + try { + URI = NetUtil.newURI(aTest.spec); + } catch (e) { + do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result); + if (aTest.fail) { + Assert.equal(e.result, aTest.result); + return; + } + do_throw(e.result); + } + + if (aTest.relativeURI) { + var relURI; + + try { + relURI = gIoService.newURI(aTest.relativeURI, null, URI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + if (aTest.relativeFail) { + Assert.equal(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info( + "relURI.pathQueryRef = " + + relURI.pathQueryRef + + ", was " + + URI.pathQueryRef + ); + URI = relURI; + do_info("URI.pathQueryRef now = " + URI.pathQueryRef); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.mutate().finalize()); + do_check_uri_eqExceptRef( + URI, + URI.mutate() + .setRef("") + .finalize() + ); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI); + + do_info( + "testing that " + + aTest.spec + + " throws or returns false " + + "from equals(null)" + ); + // XXXdholbert At some point it'd probably be worth making this behavior + // (throwing vs. returning false) consistent across URI implementations. + var threw = false; + var isEqualToNull; + try { + isEqualToNull = URI.equals(null); + } catch (e) { + threw = true; + } + Assert.ok(threw || !isEqualToNull); + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "pathQueryRef"); + do_check_property(aTest, URI, "query"); + do_check_property(aTest, URI, "ref"); + do_check_property(aTest, URI, "port"); + do_check_property(aTest, URI, "username"); + do_check_property(aTest, URI, "password"); + do_check_property(aTest, URI, "host"); + do_check_property(aTest, URI, "specIgnoringRef"); + if ("hasRef" in aTest) { + do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef); + Assert.equal(aTest.hasRef, URI.hasRef); + } +} + +// Test that a given URI parses correctly when we add a given ref to the end +function do_test_uri_with_hash_suffix(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var origURI = NetUtil.newURI(aTest.spec); + var testURI; + + if (aTest.relativeURI) { + try { + origURI = gIoService.newURI(aTest.relativeURI, null, origURI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + return; + } + try { + testURI = gIoService.newURI(aSuffix, null, origURI); + } catch (e) { + do_info( + "Caught error adding suffix to " + + aTest.spec + + " + " + + aTest.relativeURI + + ", suffix " + + aSuffix + + " Error: " + + e.result + ); + return; + } + } else { + testURI = NetUtil.newURI(aTest.spec + aSuffix); + } + + do_info( + "testing " + + aTest.spec + + " with '" + + aSuffix + + "' appended " + + "equals a clone of itself" + ); + do_check_uri_eq(testURI, testURI.mutate().finalize()); + + do_info( + "testing " + + aTest.spec + + " doesn't equal self with '" + + aSuffix + + "' appended" + ); + + Assert.ok(!origURI.equals(testURI)); + + do_info( + "testing " + + aTest.spec + + " is equalExceptRef to self with '" + + aSuffix + + "' appended" + ); + do_check_uri_eqExceptRef(origURI, testURI); + + Assert.equal(testURI.hasRef, true); + + if (!origURI.ref) { + // These tests fail if origURI has a ref + do_info( + "testing setRef('') on " + + testURI.spec + + " is equal to no-ref version but not equal to ref version" + ); + var cloneNoRef = testURI + .mutate() + .setRef("") + .finalize(); // we used to clone here. + do_info("cloneNoRef: " + cloneNoRef.spec + " hasRef: " + cloneNoRef.hasRef); + do_info("testURI: " + testURI.spec + " hasRef: " + testURI.hasRef); + do_check_uri_eq(cloneNoRef, origURI); + Assert.ok(!cloneNoRef.equals(testURI)); + + do_info( + "testing cloneWithNewRef on " + + testURI.spec + + " with an empty ref is equal to no-ref version but not equal to ref version" + ); + var cloneNewRef = testURI + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(cloneNewRef, origURI); + do_check_uri_eq(cloneNewRef, cloneNoRef); + Assert.ok(!cloneNewRef.equals(testURI)); + + do_info( + "testing cloneWithNewRef on " + + origURI.spec + + " with the same new ref is equal to ref version and not equal to no-ref version" + ); + cloneNewRef = origURI + .mutate() + .setRef(aSuffix) + .finalize(); + do_check_uri_eq(cloneNewRef, testURI); + Assert.ok(cloneNewRef.equals(testURI)); + } + + do_check_property(aTest, testURI, "scheme"); + do_check_property(aTest, testURI, "prePath"); + if (!origURI.ref) { + // These don't work if it's a ref already because '+' doesn't give the right result + do_check_property(aTest, testURI, "pathQueryRef", function(aStr) { + return aStr + aSuffix; + }); + do_check_property(aTest, testURI, "ref", function(aStr) { + return aSuffix.substr(1); + }); + } +} + +// Tests various ways of setting & clearing a ref on a URI. +function do_test_mutate_ref(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix); + var refURIWithoutSuffix = NetUtil.newURI(aTest.spec); + + var testURI = NetUtil.newURI(aTest.spec); + + // First: Try setting .ref to our suffix + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + aSuffix + + "' does what we expect" + ); + testURI = testURI + .mutate() + .setRef(aSuffix) + .finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + + // Now try setting .ref but leave off the initial hash (expect same result) + var suffixLackingHash = aSuffix.substr(1); + if (suffixLackingHash) { + // (skip this our suffix was *just* a #) + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + suffixLackingHash + + "' does what we expect" + ); + testURI = testURI + .mutate() + .setRef(suffixLackingHash) + .finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + } + + // Now, clear .ref (should get us back the original spec) + do_info( + "testing that clearing .ref on " + testURI.spec + " does what we expect" + ); + testURI = testURI + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + if (!aTest.relativeURI) { + // TODO: These tests don't work as-is for relative URIs. + + // Now try setting .spec directly (including suffix) and then clearing .ref + var specWithSuffix = aTest.spec + aSuffix; + do_info( + "testing that setting spec to " + + specWithSuffix + + " and then clearing ref does what we expect" + ); + + testURI = testURI + .mutate() + .setSpec(specWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part. + if (!(testURI instanceof Ci.nsIJARURI)) { + // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref + // (same as above, but with now with .pathQueryRef instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.pathQueryRef + aSuffix; + do_info( + "testing that setting path to " + + pathWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .pathQueryRef also clears .ref + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .finalize(); + do_info( + "testing that clearing path from " + + pathWithSuffix + + " also clears .ref" + ); + testURI = testURI + .mutate() + .setPathQueryRef("") + .finalize(); + Assert.equal(testURI.ref, ""); + } + } +} + +// Check that changing nested/about URIs works correctly. +add_task(function check_nested_mutations() { + // nsNestedAboutURI + let uri1 = gIoService.newURI("about:blank#"); + let uri2 = gIoService.newURI("about:blank"); + let uri3 = uri1 + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setRef("#") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("about:blank?something"); + uri2 = gIoService.newURI("about:blank"); + uri3 = uri1 + .mutate() + .setQuery("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setQuery("something") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("about:blank?query#ref"); + uri2 = gIoService.newURI("about:blank"); + uri3 = uri1 + .mutate() + .setPathQueryRef("blank") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setPathQueryRef("blank?query#ref") + .finalize(); + do_check_uri_eq(uri3, uri1); + + // nsSimpleNestedURI + uri1 = gIoService.newURI("view-source:http://example.com/path#"); + uri2 = gIoService.newURI("view-source:http://example.com/path"); + uri3 = uri1 + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setRef("#") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("view-source:http://example.com/path?something"); + uri2 = gIoService.newURI("view-source:http://example.com/path"); + uri3 = uri1 + .mutate() + .setQuery("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setQuery("something") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("view-source:http://example.com/path?query#ref"); + uri2 = gIoService.newURI("view-source:http://example.com/path"); + uri3 = uri1 + .mutate() + .setPathQueryRef("path") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setPathQueryRef("path?query#ref") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("view-source:about:blank#"); + uri2 = gIoService.newURI("view-source:about:blank"); + uri3 = uri1 + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setRef("#") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("view-source:about:blank?something"); + uri2 = gIoService.newURI("view-source:about:blank"); + uri3 = uri1 + .mutate() + .setQuery("") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setQuery("something") + .finalize(); + do_check_uri_eq(uri3, uri1); + + uri1 = gIoService.newURI("view-source:about:blank?query#ref"); + uri2 = gIoService.newURI("view-source:about:blank"); + uri3 = uri1 + .mutate() + .setPathQueryRef("blank") + .finalize(); + do_check_uri_eq(uri3, uri2); + uri3 = uri2 + .mutate() + .setPathQueryRef("blank?query#ref") + .finalize(); + do_check_uri_eq(uri3, uri1); +}); + +add_task(function check_space_escaping() { + let uri = gIoService.newURI("data:text/plain,hello%20world#space hash"); + Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash"); + uri = gIoService.newURI("data:text/plain,hello%20world#space%20hash"); + Assert.equal(uri.spec, "data:text/plain,hello%20world#space%20hash"); + uri = gIoService.newURI("data:text/plain,hello world#space%20hash"); + Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash"); + uri = gIoService.newURI("data:text/plain,hello world#space hash"); + Assert.equal(uri.spec, "data:text/plain,hello world#space%20hash"); + uri = gIoService.newURI("http://example.com/test path#test path"); + uri = gIoService.newURI("http://example.com/test%20path#test%20path"); +}); + +add_task(function check_schemeIsNull() { + let uri = gIoService.newURI("data:text/plain,aaa"); + Assert.ok(!uri.schemeIs(null)); + uri = gIoService.newURI("http://example.com"); + Assert.ok(!uri.schemeIs(null)); + uri = gIoService.newURI("dummyscheme://example.com"); + Assert.ok(!uri.schemeIs(null)); + uri = gIoService.newURI("jar:resource://gre/chrome.toolkit.jar!/"); + Assert.ok(!uri.schemeIs(null)); + uri = gIoService.newURI("moz-icon://.unknown?size=32"); + Assert.ok(!uri.schemeIs(null)); +}); + +// Check that characters in the query of moz-extension aren't improperly unescaped (Bug 1547882) +add_task(function check_mozextension_query() { + let uri = gIoService.newURI( + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html" + ); + uri = uri + .mutate() + .setQuery("u=https%3A%2F%2Fnews.ycombinator.com%2F") + .finalize(); + Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F"); + uri = gIoService.newURI( + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F" + ); + Assert.equal( + uri.spec, + "moz-extension://a7d1572e-3beb-4d93-a920-c408fa09e8ea/_source/holding.html?u=https%3A%2F%2Fnews.ycombinator.com%2F" + ); + Assert.equal(uri.query, "u=https%3A%2F%2Fnews.ycombinator.com%2F"); +}); + +add_task(function check_resolve() { + let base = gIoService.newURI("http://example.com"); + let uri = gIoService.newURI("tel::+371 27028456", "utf-8", base); + Assert.equal(uri.spec, "tel::+371 27028456"); +}); + +add_task(function test_extra_protocols() { + // dweb:// + let url = gIoService.newURI("dweb://example.com/test"); + Assert.equal(url.host, "example.com"); + + // dat:// + url = gIoService.newURI( + "dat://41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5/" + ); + Assert.equal( + url.host, + "41f8a987cfeba80a037e51cc8357d513b62514de36f2f9b3d3eeec7a8fb3b5a5" + ); + url = gIoService.newURI("dat://example.com/test"); + Assert.equal(url.host, "example.com"); + + // ipfs:// + url = gIoService.newURI( + "ipfs://bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy/frontend/license.txt" + ); + Assert.equal(url.scheme, "ipfs"); + Assert.equal( + url.host, + "bafybeiccfclkdtucu6y4yc5cpr6y3yuinr67svmii46v5cfcrkp47ihehy" + ); + Assert.equal(url.filePath, "/frontend/license.txt"); + + // ipns:// + url = gIoService.newURI("ipns://peerdium.gozala.io/index.html"); + Assert.equal(url.scheme, "ipns"); + Assert.equal(url.host, "peerdium.gozala.io"); + Assert.equal(url.filePath, "/index.html"); + + // ssb:// + url = gIoService.newURI("ssb://scuttlebutt.nz/index.html"); + Assert.equal(url.scheme, "ssb"); + Assert.equal(url.host, "scuttlebutt.nz"); + Assert.equal(url.filePath, "/index.html"); + + // wtp:// + url = gIoService.newURI( + "wtp://951ead31d09e4049fc1f21f137e233dd0589fcbd/blog/vim-tips/" + ); + Assert.equal(url.scheme, "wtp"); + Assert.equal(url.host, "951ead31d09e4049fc1f21f137e233dd0589fcbd"); + Assert.equal(url.filePath, "/blog/vim-tips/"); +}); + +// TEST MAIN FUNCTION +// ------------------ +add_task(function mainTest() { + // UTF-8 check - From bug 622981 + // ASCII + let base = gIoService.newURI("http://example.org/xenia?"); + let resolved = gIoService.newURI("?x", null, base); + let expected = gIoService.newURI("http://example.org/xenia?x"); + do_info( + "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = gIoService.newURI("http://example.org/xènia?"); + resolved = gIoService.newURI("?x", null, base); + expected = gIoService.newURI("http://example.org/xènia?x"); + do_info( + "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + gTests.forEach(function(aTest) { + // Check basic URI functionality + do_test_uri_basic(aTest); + + if (!aTest.fail) { + // Try adding various #-prefixed strings to the ends of the URIs + gHashSuffixes.forEach(function(aSuffix) { + do_test_uri_with_hash_suffix(aTest, aSuffix); + if (!aTest.immutable) { + do_test_mutate_ref(aTest, aSuffix); + } + }); + + // For URIs that we couldn't mutate above due to them being immutable: + // Now we check that they're actually immutable. + if (aTest.immutable) { + Assert.ok(aTest.immutable); + } + } + }); +}); + +function check_round_trip_serialization(spec) { + dump(`checking ${spec}\n`); + let uri = gIoService.newURI(spec); + let str = serialize_to_escaped_string(uri); + let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI); + equal(other.spec, uri.spec); +} + +add_task(function test_iconURI_serialization() { + // URIs taken from test_moz_icon_uri.js + + let tests = [ + "moz-icon://foo.html?contentType=bar&size=button&state=normal", + "moz-icon://foo.html?size=3", + "moz-icon://stock/foo", + "moz-icon:file://foo.txt", + "moz-icon://file://foo.txt", + ]; + + tests.forEach(str => check_round_trip_serialization(str)); +}); + +add_task(function test_jarURI_serialization() { + check_round_trip_serialization("jar:http://example.com/bar.jar!/"); +}); diff --git a/netwerk/test/unit/test_URIs2.js b/netwerk/test/unit/test_URIs2.js new file mode 100644 index 0000000000..997f63658e --- /dev/null +++ b/netwerk/test/unit/test_URIs2.js @@ -0,0 +1,906 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +var gIoService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +// Run by: cd objdir; make -C netwerk/test/ xpcshell-tests +// or: cd objdir; make SOLO_FILE="test_URIs2.js" -C netwerk/test/ check-one + +// This is a clone of test_URIs.js, with a different set of test data in gTests. +// The original test data in test_URIs.js was split between test_URIs and test_URIs2.js +// because test_URIs.js was running for too long on slow platforms, causing +// intermittent timeouts. + +// Relevant RFCs: 1738, 1808, 2396, 3986 (newer than the code) +// http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 +// http://greenbytes.de/tech/tc/uris/ + +// TEST DATA +// --------- +var gTests = [ + { + spec: "view-source:about:blank", + scheme: "view-source", + prePath: "view-source:", + pathQueryRef: "about:blank", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "view-source:http://www.mozilla.org/", + scheme: "view-source", + prePath: "view-source:", + pathQueryRef: "http://www.mozilla.org/", + ref: "", + nsIURL: false, + nsINestedURI: true, + immutable: true, + }, + { + spec: "x-external:", + scheme: "x-external", + prePath: "x-external:", + pathQueryRef: "", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "x-external:abc", + scheme: "x-external", + prePath: "x-external:", + pathQueryRef: "abc", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://www2.example.com/", + relativeURI: "a/b/c/d", + scheme: "http", + prePath: "http://www2.example.com", + pathQueryRef: "/a/b/c/d", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + // relative URL testcases from http://greenbytes.de/tech/webdav/rfc3986.html#rfc.section.5.4 + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g:h", + scheme: "g", + prePath: "g:", + pathQueryRef: "h", + ref: "", + nsIURL: false, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "?y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;p?y", + ref: "", // fix + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g?y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g?y", + ref: "", // fix + specIgnoringRef: "http://a/b/c/g?y", + hasRef: false, + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;p?q#s", + ref: "s", // fix + specIgnoringRef: "http://a/b/c/d;p?q", + hasRef: true, + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g?y#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g?y#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + /* + Bug xxxxxx - we return a path of b/c/;x + { spec: "http://a/b/c/d;p?q", + relativeURI: ";x", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d;x", + ref: "", + nsIURL: true, nsINestedURI: false }, + */ + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x?y#s", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x?y#s", + ref: "s", + nsIURL: true, + nsINestedURI: false, + }, + /* + Can't easily specify a relative URI of "" to the test code + { spec: "http://a/b/c/d;p?q", + relativeURI: "", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/d", + ref: "", + nsIURL: true, nsINestedURI: false }, + */ + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // abnormal examples + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "../../../../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + + // coalesce + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/./g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "/../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g.", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g.", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/.g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g..", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g..", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "..g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/..g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: ".", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./../g", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/g", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "./g/.", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/./h", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g/h", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g/../h", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/h", + ref: "", // fix + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x=1/./y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/g;x=1/y", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "http://a/b/c/d;p?q", + relativeURI: "g;x=1/../y", + scheme: "http", + prePath: "http://a", + pathQueryRef: "/b/c/y", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + // protocol-relative http://tools.ietf.org/html/rfc3986#section-4.2 + { + spec: "http://www2.example.com/", + relativeURI: "//www3.example2.com/bar", + scheme: "http", + prePath: "http://www3.example2.com", + pathQueryRef: "/bar", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, + { + spec: "https://www2.example.com/", + relativeURI: "//www3.example2.com/bar", + scheme: "https", + prePath: "https://www3.example2.com", + pathQueryRef: "/bar", + ref: "", + nsIURL: true, + nsINestedURI: false, + }, +]; + +var gHashSuffixes = ["#", "#myRef", "#myRef?a=b", "#myRef#", "#myRef#x:yz"]; + +// TEST HELPER FUNCTIONS +// --------------------- +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +// Checks that the URIs satisfy equals(), in both possible orderings. +// Also checks URI.equalsExceptRef(), because equal URIs should also be equal +// when we ignore the ref. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eq(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info("(uri equals check: '" + aURI1.spec + "' == '" + aURI2.spec + "')"); + aCheckTrueFunc(aURI1.equals(aURI2)); + do_info("(uri equals check: '" + aURI2.spec + "' == '" + aURI1.spec + "')"); + aCheckTrueFunc(aURI2.equals(aURI1)); + + // (Only take the extra step of testing 'equalsExceptRef' when we expect the + // URIs to really be equal. In 'todo' cases, the URIs may or may not be + // equal when refs are ignored - there's no way of knowing in general.) + if (aCheckTrueFunc == ok) { + do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc); + } +} + +// Checks that the URIs satisfy equalsExceptRef(), in both possible orderings. +// +// The third argument is optional. If the client passes a third argument +// (e.g. todo_check_true), we'll use that in lieu of ok. +function do_check_uri_eqExceptRef(aURI1, aURI2, aCheckTrueFunc = ok) { + do_info( + "(uri equalsExceptRef check: '" + aURI1.spec + "' == '" + aURI2.spec + "')" + ); + aCheckTrueFunc(aURI1.equalsExceptRef(aURI2)); + do_info( + "(uri equalsExceptRef check: '" + aURI2.spec + "' == '" + aURI1.spec + "')" + ); + aCheckTrueFunc(aURI2.equalsExceptRef(aURI1)); +} + +// Checks that the given property on aURI matches the corresponding property +// in the test bundle (or matches some function of that corresponding property, +// if aTestFunctor is passed in). +function do_check_property(aTest, aURI, aPropertyName, aTestFunctor) { + if (aTest[aPropertyName]) { + var expectedVal = aTestFunctor + ? aTestFunctor(aTest[aPropertyName]) + : aTest[aPropertyName]; + + do_info( + "testing " + + aPropertyName + + " of " + + (aTestFunctor ? "modified '" : "'") + + aTest.spec + + "' is '" + + expectedVal + + "'" + ); + Assert.equal(aURI[aPropertyName], expectedVal); + } +} + +// Test that a given URI parses correctly into its various components. +function do_test_uri_basic(aTest) { + var URI; + + do_info( + "Basic tests for " + aTest.spec + " relative URI: " + aTest.relativeURI + ); + + try { + URI = NetUtil.newURI(aTest.spec); + } catch (e) { + do_info("Caught error on parse of" + aTest.spec + " Error: " + e.result); + if (aTest.fail) { + Assert.equal(e.result, aTest.result); + return; + } + do_throw(e.result); + } + + if (aTest.relativeURI) { + var relURI; + + try { + relURI = gIoService.newURI(aTest.relativeURI, null, URI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + if (aTest.relativeFail) { + Assert.equal(e.result, aTest.relativeFail); + return; + } + do_throw(e.result); + } + do_info( + "relURI.pathQueryRef = " + + relURI.pathQueryRef + + ", was " + + URI.pathQueryRef + ); + URI = relURI; + do_info("URI.pathQueryRef now = " + URI.pathQueryRef); + } + + // Sanity-check + do_info("testing " + aTest.spec + " equals a clone of itself"); + do_check_uri_eq(URI, URI.mutate().finalize()); + do_check_uri_eqExceptRef( + URI, + URI.mutate() + .setRef("") + .finalize() + ); + do_info("testing " + aTest.spec + " instanceof nsIURL"); + Assert.equal(URI instanceof Ci.nsIURL, aTest.nsIURL); + do_info("testing " + aTest.spec + " instanceof nsINestedURI"); + Assert.equal(URI instanceof Ci.nsINestedURI, aTest.nsINestedURI); + + do_info( + "testing that " + + aTest.spec + + " throws or returns false " + + "from equals(null)" + ); + // XXXdholbert At some point it'd probably be worth making this behavior + // (throwing vs. returning false) consistent across URI implementations. + var threw = false; + var isEqualToNull; + try { + isEqualToNull = URI.equals(null); + } catch (e) { + threw = true; + } + Assert.ok(threw || !isEqualToNull); + + // Check the various components + do_check_property(aTest, URI, "scheme"); + do_check_property(aTest, URI, "prePath"); + do_check_property(aTest, URI, "pathQueryRef"); + do_check_property(aTest, URI, "query"); + do_check_property(aTest, URI, "ref"); + do_check_property(aTest, URI, "port"); + do_check_property(aTest, URI, "username"); + do_check_property(aTest, URI, "password"); + do_check_property(aTest, URI, "host"); + do_check_property(aTest, URI, "specIgnoringRef"); + if ("hasRef" in aTest) { + do_info("testing hasref: " + aTest.hasRef + " vs " + URI.hasRef); + Assert.equal(aTest.hasRef, URI.hasRef); + } +} + +// Test that a given URI parses correctly when we add a given ref to the end +function do_test_uri_with_hash_suffix(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var origURI = NetUtil.newURI(aTest.spec); + var testURI; + + if (aTest.relativeURI) { + try { + origURI = gIoService.newURI(aTest.relativeURI, null, origURI); + } catch (e) { + do_info( + "Caught error on Relative parse of " + + aTest.spec + + " + " + + aTest.relativeURI + + " Error: " + + e.result + ); + return; + } + try { + testURI = gIoService.newURI(aSuffix, null, origURI); + } catch (e) { + do_info( + "Caught error adding suffix to " + + aTest.spec + + " + " + + aTest.relativeURI + + ", suffix " + + aSuffix + + " Error: " + + e.result + ); + return; + } + } else { + testURI = NetUtil.newURI(aTest.spec + aSuffix); + } + + do_info( + "testing " + + aTest.spec + + " with '" + + aSuffix + + "' appended " + + "equals a clone of itself" + ); + do_check_uri_eq(testURI, testURI.mutate().finalize()); + + do_info( + "testing " + + aTest.spec + + " doesn't equal self with '" + + aSuffix + + "' appended" + ); + + Assert.ok(!origURI.equals(testURI)); + + do_info( + "testing " + + aTest.spec + + " is equalExceptRef to self with '" + + aSuffix + + "' appended" + ); + do_check_uri_eqExceptRef(origURI, testURI); + + Assert.equal(testURI.hasRef, true); + + if (!origURI.ref) { + // These tests fail if origURI has a ref + do_info( + "testing cloneIgnoringRef on " + + testURI.spec + + " is equal to no-ref version but not equal to ref version" + ); + var cloneNoRef = testURI + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(cloneNoRef, origURI); + Assert.ok(!cloneNoRef.equals(testURI)); + } + + do_check_property(aTest, testURI, "scheme"); + do_check_property(aTest, testURI, "prePath"); + if (!origURI.ref) { + // These don't work if it's a ref already because '+' doesn't give the right result + do_check_property(aTest, testURI, "pathQueryRef", function(aStr) { + return aStr + aSuffix; + }); + do_check_property(aTest, testURI, "ref", function(aStr) { + return aSuffix.substr(1); + }); + } +} + +// Tests various ways of setting & clearing a ref on a URI. +function do_test_mutate_ref(aTest, aSuffix) { + do_info("making sure caller is using suffix that starts with '#'"); + Assert.equal(aSuffix[0], "#"); + + var refURIWithSuffix = NetUtil.newURI(aTest.spec + aSuffix); + var refURIWithoutSuffix = NetUtil.newURI(aTest.spec); + + var testURI = NetUtil.newURI(aTest.spec); + + // First: Try setting .ref to our suffix + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + aSuffix + + "' does what we expect" + ); + testURI = testURI + .mutate() + .setRef(aSuffix) + .finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + + // Now try setting .ref but leave off the initial hash (expect same result) + var suffixLackingHash = aSuffix.substr(1); + if (suffixLackingHash) { + // (skip this our suffix was *just* a #) + do_info( + "testing that setting .ref on " + + aTest.spec + + " to '" + + suffixLackingHash + + "' does what we expect" + ); + testURI = testURI + .mutate() + .setRef(suffixLackingHash) + .finalize(); + do_check_uri_eq(testURI, refURIWithSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithoutSuffix); + } + + // Now, clear .ref (should get us back the original spec) + do_info( + "testing that clearing .ref on " + testURI.spec + " does what we expect" + ); + testURI = testURI + .mutate() + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + if (!aTest.relativeURI) { + // TODO: These tests don't work as-is for relative URIs. + + // Now try setting .spec directly (including suffix) and then clearing .ref + var specWithSuffix = aTest.spec + aSuffix; + do_info( + "testing that setting spec to " + + specWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI + .mutate() + .setSpec(specWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // XXX nsIJARURI throws an exception in SetPath(), so skip it for next part. + if (!(testURI instanceof Ci.nsIJARURI)) { + // Now try setting .pathQueryRef directly (including suffix) and then clearing .ref + // (same as above, but with now with .pathQueryRef instead of .spec) + testURI = NetUtil.newURI(aTest.spec); + + var pathWithSuffix = aTest.pathQueryRef + aSuffix; + do_info( + "testing that setting path to " + + pathWithSuffix + + " and then clearing ref does what we expect" + ); + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .setRef("") + .finalize(); + do_check_uri_eq(testURI, refURIWithoutSuffix); + do_check_uri_eqExceptRef(testURI, refURIWithSuffix); + + // Also: make sure that clearing .pathQueryRef also clears .ref + testURI = testURI + .mutate() + .setPathQueryRef(pathWithSuffix) + .finalize(); + do_info( + "testing that clearing path from " + + pathWithSuffix + + " also clears .ref" + ); + testURI = testURI + .mutate() + .setPathQueryRef("") + .finalize(); + Assert.equal(testURI.ref, ""); + } + } +} + +// TEST MAIN FUNCTION +// ------------------ +function run_test() { + // UTF-8 check - From bug 622981 + // ASCII + let base = gIoService.newURI("http://example.org/xenia?"); + let resolved = gIoService.newURI("?x", null, base); + let expected = gIoService.newURI("http://example.org/xenia?x"); + do_info( + "Bug 662981: ACSII - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + // UTF-8 character "è" + // Bug 622981 was triggered by an empty query string + base = gIoService.newURI("http://example.org/xènia?"); + resolved = gIoService.newURI("?x", null, base); + expected = gIoService.newURI("http://example.org/xènia?x"); + do_info( + "Bug 662981: UTF8 - comparing " + resolved.spec + " and " + expected.spec + ); + Assert.ok(resolved.equals(expected)); + + gTests.forEach(function(aTest) { + // Check basic URI functionality + do_test_uri_basic(aTest); + + if (!aTest.fail) { + // Try adding various #-prefixed strings to the ends of the URIs + gHashSuffixes.forEach(function(aSuffix) { + do_test_uri_with_hash_suffix(aTest, aSuffix); + if (!aTest.immutable) { + do_test_mutate_ref(aTest, aSuffix); + } + }); + + // For URIs that we couldn't mutate above due to them being immutable: + // Now we check that they're actually immutable. + if (aTest.immutable) { + Assert.ok(aTest.immutable); + } + } + }); +} diff --git a/netwerk/test/unit/test_XHR_redirects.js b/netwerk/test/unit/test_XHR_redirects.js new file mode 100644 index 0000000000..dfe037d0ec --- /dev/null +++ b/netwerk/test/unit/test_XHR_redirects.js @@ -0,0 +1,273 @@ +// This file tests whether XmlHttpRequests correctly handle redirects, +// including rewriting POSTs to GETs (on 301/302/303), as well as +// prompting for redirects of other unsafe methods (such as PUTs, DELETEs, +// etc--see HttpBaseChannel::IsSafeMethod). Since no prompting is possible +// in xpcshell, we get an error for prompts, and the request fails. +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { Preferences } = ChromeUtils.import( + "resource://gre/modules/Preferences.jsm" +); + +var sSame; +var sOther; +var sRedirectPromptPref; + +const BUGID = "676059"; +const OTHERBUGID = "696849"; + +XPCOMUtils.defineLazyGetter(this, "pSame", function() { + return sSame.identity.primaryPort; +}); +XPCOMUtils.defineLazyGetter(this, "pOther", function() { + return sOther.identity.primaryPort; +}); + +function createXHR(async, method, path) { + var xhr = new XMLHttpRequest(); + xhr.open(method, "http://localhost:" + pSame + path, async); + return xhr; +} + +function checkResults(xhr, method, status, unsafe) { + if (unsafe) { + if (sRedirectPromptPref) { + // The method is null if we prompt for unsafe redirects + method = null; + } else { + // The status code is 200 when we don't prompt for unsafe redirects + status = 200; + } + } + + if (xhr.readyState != 4) { + return false; + } + Assert.equal(xhr.status, status); + + if (status == 200) { + // if followed then check for echoed method name + Assert.equal(xhr.getResponseHeader("X-Received-Method"), method); + } + + return true; +} + +function run_test() { + // start servers + sSame = new HttpServer(); + + // same-origin redirects + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect301", + bug676059redirect301 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect302", + bug676059redirect302 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect303", + bug676059redirect303 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect307", + bug676059redirect307 + ); + sSame.registerPathHandler( + "/bug" + BUGID + "-redirect308", + bug676059redirect308 + ); + + // cross-origin redirects + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect301", + bug696849redirect301 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect302", + bug696849redirect302 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect303", + bug696849redirect303 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect307", + bug696849redirect307 + ); + sSame.registerPathHandler( + "/bug" + OTHERBUGID + "-redirect308", + bug696849redirect308 + ); + + // same-origin target + sSame.registerPathHandler("/bug" + BUGID + "-target", echoMethod); + sSame.start(-1); + + // cross-origin target + sOther = new HttpServer(); + sOther.registerPathHandler("/bug" + OTHERBUGID + "-target", echoMethod); + sOther.start(-1); + + // format: redirectType, methodToSend, redirectedMethod, finalStatus + // redirectType sets the URI the initial request goes to + // methodToSend is the HTTP method to send + // redirectedMethod is the method to use for the redirect, if any + // finalStatus is 200 when the redirect takes place, redirectType otherwise + + // Note that unsafe methods should not follow the redirect automatically + // Of the methods below, DELETE, POST and PUT are unsafe + + sRedirectPromptPref = Preferences.get("network.http.prompt-temp-redirect"); + // Following Bug 677754 we don't prompt for unsafe redirects + + // same-origin variant + var tests = [ + // 301: rewrite just POST + [301, "DELETE", "DELETE", 301, true], + [301, "GET", "GET", 200, false], + [301, "HEAD", "HEAD", 200, false], + [301, "POST", "GET", 200, false], + [301, "PUT", "PUT", 301, true], + [301, "PROPFIND", "PROPFIND", 200, false], + // 302: see 301 + [302, "DELETE", "DELETE", 302, true], + [302, "GET", "GET", 200, false], + [302, "HEAD", "HEAD", 200, false], + [302, "POST", "GET", 200, false], + [302, "PUT", "PUT", 302, true], + [302, "PROPFIND", "PROPFIND", 200, false], + // 303: rewrite to GET except HEAD + [303, "DELETE", "GET", 200, false], + [303, "GET", "GET", 200, false], + [303, "HEAD", "HEAD", 200, false], + [303, "POST", "GET", 200, false], + [303, "PUT", "GET", 200, false], + [303, "PROPFIND", "GET", 200, false], + // 307: never rewrite + [307, "DELETE", "DELETE", 307, true], + [307, "GET", "GET", 200, false], + [307, "HEAD", "HEAD", 200, false], + [307, "POST", "POST", 307, true], + [307, "PUT", "PUT", 307, true], + [307, "PROPFIND", "PROPFIND", 200, false], + // 308: never rewrite + [308, "DELETE", "DELETE", 308, true], + [308, "GET", "GET", 200, false], + [308, "HEAD", "HEAD", 200, false], + [308, "POST", "POST", 308, true], + [308, "PUT", "PUT", 308, true], + [308, "PROPFIND", "PROPFIND", 200, false], + ]; + + // cross-origin variant + var othertests = tests; // for now these have identical results + + var xhr; + + for (var i = 0; i < tests.length; ++i) { + dump("Testing " + tests[i] + "\n"); + xhr = createXHR( + false, + tests[i][1], + "/bug" + BUGID + "-redirect" + tests[i][0] + ); + xhr.send(null); + checkResults(xhr, tests[i][2], tests[i][3], tests[i][4]); + } + + for (var i = 0; i < othertests.length; ++i) { + dump("Testing " + othertests[i] + " (cross-origin)\n"); + xhr = createXHR( + false, + othertests[i][1], + "/bug" + OTHERBUGID + "-redirect" + othertests[i][0] + ); + xhr.send(null); + checkResults(xhr, othertests[i][2], tests[i][3], tests[i][4]); + } + + sSame.stop(do_test_finished); + sOther.stop(do_test_finished); +} + +function redirect(metadata, response, status, port, bugid) { + // set a proper reason string to avoid confusion when looking at the + // HTTP messages + var reason; + if (status == 301) { + reason = "Moved Permanently"; + } else if (status == 302) { + reason = "Found"; + } else if (status == 303) { + reason = "See Other"; + } else if (status == 307) { + reason = "Temporary Redirect"; + } else if (status == 308) { + reason = "Permanent Redirect"; + } + + response.setStatusLine(metadata.httpVersion, status, reason); + response.setHeader( + "Location", + "http://localhost:" + port + "/bug" + bugid + "-target" + ); +} + +// PATH HANDLER FOR /bug676059-redirect301 +function bug676059redirect301(metadata, response) { + redirect(metadata, response, 301, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect301 +function bug696849redirect301(metadata, response) { + redirect(metadata, response, 301, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect302 +function bug676059redirect302(metadata, response) { + redirect(metadata, response, 302, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect302 +function bug696849redirect302(metadata, response) { + redirect(metadata, response, 302, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect303 +function bug676059redirect303(metadata, response) { + redirect(metadata, response, 303, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect303 +function bug696849redirect303(metadata, response) { + redirect(metadata, response, 303, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug676059-redirect307 +function bug676059redirect307(metadata, response) { + redirect(metadata, response, 307, pSame, BUGID); +} + +// PATH HANDLER FOR /bug676059-redirect308 +function bug676059redirect308(metadata, response) { + redirect(metadata, response, 308, pSame, BUGID); +} + +// PATH HANDLER FOR /bug696849-redirect307 +function bug696849redirect307(metadata, response) { + redirect(metadata, response, 307, pOther, OTHERBUGID); +} + +// PATH HANDLER FOR /bug696849-redirect308 +function bug696849redirect308(metadata, response) { + redirect(metadata, response, 308, pOther, OTHERBUGID); +} + +// Echo the request method in "X-Received-Method" header field +function echoMethod(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("X-Received-Method", metadata.method); +} diff --git a/netwerk/test/unit/test_about_networking.js b/netwerk/test/unit/test_about_networking.js new file mode 100644 index 0000000000..186e2a539a --- /dev/null +++ b/netwerk/test/unit/test_about_networking.js @@ -0,0 +1,121 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const gDashboard = Cc["@mozilla.org/network/dashboard;1"].getService( + Ci.nsIDashboard +); + +const gServerSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket +); +const gHttpServer = new HttpServer(); + +add_test(function test_http() { + gDashboard.requestHttpConnections(function(data) { + let found = false; + for (let i = 0; i < data.connections.length; i++) { + if (data.connections[i].host == "localhost") { + found = true; + break; + } + } + Assert.equal(found, true); + + run_next_test(); + }); +}); + +add_test(function test_dns() { + gDashboard.requestDNSInfo(function(data) { + let found = false; + for (let i = 0; i < data.entries.length; i++) { + if (data.entries[i].hostname == "localhost") { + found = true; + break; + } + } + Assert.equal(found, true); + + do_test_pending(); + gHttpServer.stop(do_test_finished); + + run_next_test(); + }); +}); + +add_test(function test_sockets() { + // TODO: enable this test in bug 1581892. + if (mozinfo.socketprocess_networking) { + info("skip test_sockets"); + run_next_test(); + return; + } + + let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService( + Ci.nsISocketTransportService + ); + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + + let transport = sts.createTransport( + [], + "127.0.0.1", + gServerSocket.port, + null + ); + let listener = { + onTransportStatus(aTransport, aStatus, aProgress, aProgressMax) { + if (aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) { + gDashboard.requestSockets(function(data) { + gServerSocket.close(); + let found = false; + for (let i = 0; i < data.sockets.length; i++) { + if (data.sockets[i].host == "127.0.0.1") { + found = true; + break; + } + } + Assert.equal(found, true); + + run_next_test(); + }); + } + }, + }; + transport.setEventSink(listener, threadManager.currentThread); + + transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0); +}); + +function run_test() { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // We always resolve localhost as it's hardcoded without the following pref: + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + let ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + gHttpServer.start(-1); + + let uri = ioService.newURI( + "http://localhost:" + gHttpServer.identity.primaryPort + ); + let channel = NetUtil.newChannel({ uri, loadUsingSystemPrincipal: true }); + + channel.open(); + + gServerSocket.init(-1, true, -1); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + run_next_test(); +} diff --git a/netwerk/test/unit/test_about_protocol.js b/netwerk/test/unit/test_about_protocol.js new file mode 100644 index 0000000000..2da50acad3 --- /dev/null +++ b/netwerk/test/unit/test_about_protocol.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var unsafeAboutModule = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + newChannel(aURI, aLoadInfo) { + var uri = Services.io.newURI("about:blank"); + let chan = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo); + chan.owner = Services.scriptSecurityManager.getSystemPrincipal(); + return chan; + }, + getURIFlags(aURI) { + return Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT; + }, +}; + +var factory = { + createInstance(aOuter, aIID) { + if (aOuter) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + return unsafeAboutModule.QueryInterface(aIID); + }, + lockFactory(aLock) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + QueryInterface: ChromeUtils.generateQI(["nsIFactory"]), +}; + +function run_test() { + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + let classID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID(); + registrar.registerFactory( + classID, + "", + "@mozilla.org/network/protocol/about;1?what=unsafe", + factory + ); + + let aboutUnsafeChan = NetUtil.newChannel({ + uri: "about:unsafe", + loadUsingSystemPrincipal: true, + }); + + Assert.equal( + null, + aboutUnsafeChan.owner, + "URI_SAFE_FOR_UNTRUSTED_CONTENT channel has no owner" + ); + + registrar.unregisterFactory(classID, factory); +} diff --git a/netwerk/test/unit/test_aboutblank.js b/netwerk/test/unit/test_aboutblank.js new file mode 100644 index 0000000000..2d5c92f095 --- /dev/null +++ b/netwerk/test/unit/test_aboutblank.js @@ -0,0 +1,32 @@ +"use strict"; + +function run_test() { + var base = NetUtil.newURI("http://www.example.com"); + var about1 = NetUtil.newURI("about:blank"); + var about2 = NetUtil.newURI("about:blank", null, base); + + var chan1 = NetUtil.newChannel({ + uri: about1, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIPropertyBag2); + + var chan2 = NetUtil.newChannel({ + uri: about2, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIPropertyBag2); + + var haveProp = false; + var propVal = null; + try { + propVal = chan1.getPropertyAsInterface("baseURI", Ci.nsIURI); + haveProp = true; + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw e; + } + // Property shouldn't be there. + } + Assert.equal(propVal, null); + Assert.equal(haveProp, false); + Assert.equal(chan2.getPropertyAsInterface("baseURI", Ci.nsIURI), base); +} diff --git a/netwerk/test/unit/test_addr_in_use_error.js b/netwerk/test/unit/test_addr_in_use_error.js new file mode 100644 index 0000000000..d25860e896 --- /dev/null +++ b/netwerk/test/unit/test_addr_in_use_error.js @@ -0,0 +1,36 @@ +// Opening a second listening socket on the same address as an extant +// socket should elicit NS_ERROR_SOCKET_ADDRESS_IN_USE on non-Windows +// machines. + +"use strict"; + +var CC = Components.Constructor; + +const ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); + +function testAddrInUse() { + // Windows lets us have as many sockets listening on the same address as + // we like, evidently. + if (mozinfo.os == "win") { + return; + } + + // Create listening socket: + // any port (-1), loopback only (true), default backlog (-1) + let listener = ServerSocket(-1, true, -1); + Assert.ok(listener instanceof Ci.nsIServerSocket); + + // Try to create another listening socket on the same port, whatever that was. + do_check_throws_nsIException( + () => ServerSocket(listener.port, true, -1), + "NS_ERROR_SOCKET_ADDRESS_IN_USE" + ); +} + +function run_test() { + testAddrInUse(); +} diff --git a/netwerk/test/unit/test_alt-data_closeWithStatus.js b/netwerk/test/unit/test_alt-data_closeWithStatus.js new file mode 100644 index 0000000000..766601ee5f --- /dev/null +++ b/netwerk/test/unit/test_alt-data_closeWithStatus.js @@ -0,0 +1,174 @@ +/** + * Test for the "alternative data stream" - closing the stream with an error. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store something in alt data (using the asyncWait method) + * - then we abort the operation calling closeWithStatus() + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - again we receive the data from the server. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +// needs to be rooted +var cacheFlushObserver = (cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + readServerContentAgain(); + }, +}); + +var currentThread = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + try { + var etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + var etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + if (!inChildProcess()) { + cache_storage = getCacheStorage("disk"); + wait_for_cache_index(asyncOpen); + } else { + asyncOpen(); + } +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + if (!inChildProcess()) { + currentThread = Services.tm.currentThread; + } + + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + + var aos = os.QueryInterface(Ci.nsIAsyncOutputStream); + aos.asyncWait( + _ => { + os.write(altContent, altContent.length); + aos.closeWithStatus(Cr.NS_ERROR_FAILURE); + executeSoon(flushAndReadServerContentAgain); + }, + 0, + 0, + currentThread + ); + }); + }); +} + +function flushAndReadServerContentAgain() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + if (!inChildProcess()) { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + readServerContentAgain(); + }); + } +} + +function readServerContentAgain() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType("dummy1", "text/javascript", true); + cc.preferAlternativeDataType(altContentType, "text/plain", true); + cc.preferAlternativeDataType("dummy2", "", true); + + chan.asyncOpen(new ChannelListener(readServerContentAgainCB, null)); +} + +function readServerContentAgainCB(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished)); +} diff --git a/netwerk/test/unit/test_alt-data_cross_process.js b/netwerk/test/unit/test_alt-data_cross_process.js new file mode 100644 index 0000000000..05959a54af --- /dev/null +++ b/netwerk/test/unit/test_alt-data_cross_process.js @@ -0,0 +1,143 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store the alt data along the channel (to the cache entry) + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - this time the alt data must arive + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + try { + var etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + var etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + asyncOpen(); +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); + }); +} + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + openAltChannel(); + }); +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(new ChannelListener(readAltContent, null)); +} + +function readAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + // FINISH + do_send_remote_message("done"); + do_await_remote_message("finish").then(() => { + httpServer.stop(do_test_finished); + }); +} diff --git a/netwerk/test/unit/test_alt-data_overwrite.js b/netwerk/test/unit/test_alt-data_overwrite.js new file mode 100644 index 0000000000..8521c13107 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_overwrite.js @@ -0,0 +1,203 @@ +/** + * Test for overwriting the alternative data in a cache entry. + * + * - run_test loads a new channel + * - readServerContent checks the content, and saves alt-data + * - cacheFlushObserver creates a new channel with "text/binary" alt-data type + * - readAltContent checks that it gets back alt-data and creates a channel with the dummy/null alt-data type + * - readServerContent2 checks that it gets regular content, from the cache and tries to overwrite the alt-data with the same representation + * - cacheFlushObserver2 creates a new channel with "text/binary" alt-data type + * - readAltContent2 checks that it gets back alt-data, and tries to overwrite with a kind of alt-data + * - cacheFlushObserver3 creates a new channel with "text/binary2" alt-data type + * - readAltContent3 checks that it gets back the newly saved alt-data + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +let httpServer = null; + +function make_and_open_channel(url, altContentType, callback) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + if (altContentType) { + let cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + } + chan.asyncOpen(new ChannelListener(callback, null)); +} + +const responseContent = "response body"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; +const altContent2 = "abc"; +const altContentType2 = "text/binary2"; + +let servedNotModified = false; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + let etag = ""; + try { + etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + etag = ""; + } + + if (etag == "test-etag1") { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + servedNotModified = false; + response.bodyOutputStream.write(responseContent, responseContent.length); + } +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + do_test_pending(); + make_and_open_channel(URL, altContentType, readServerContent); +} + +function readServerContent(request, buffer) { + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + let os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); +} + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver); +} + +// needs to be rooted +let cacheFlushObserver = { + observe() { + if (!cacheFlushObserver) { + info("ignoring cacheFlushObserver\n"); + return; + } + cacheFlushObserver = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType, readAltContent); + }, +}; + +function readAltContent(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(fromCache || servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + make_and_open_channel(URL, "dummy/null", readServerContent2); +} + +function readServerContent2(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(fromCache || servedNotModified, true); + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + let os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel2); + }); +} + +function flushAndOpenAltChannel2() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver2); +} + +// needs to be rooted +let cacheFlushObserver2 = { + observe() { + if (!cacheFlushObserver2) { + info("ignoring cacheFlushObserver2\n"); + return; + } + cacheFlushObserver2 = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType, readAltContent2); + }, +}; + +function readAltContent2(request, buffer, closure, fromCache) { + Cu.forceShrinkingGC(); + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified || fromCache, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + + executeSoon(() => { + Cu.forceShrinkingGC(); + info("writing other content\n"); + let os = cc.openAlternativeOutputStream( + altContentType2, + altContent2.length + ); + os.write(altContent2, altContent2.length); + os.close(); + + executeSoon(flushAndOpenAltChannel3); + }); +} + +function flushAndOpenAltChannel3() { + // We need to do a GC pass to ensure the cache entry has been freed. + Cu.forceShrinkingGC(); + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(cacheFlushObserver3); +} + +// needs to be rooted +let cacheFlushObserver3 = { + observe() { + if (!cacheFlushObserver3) { + info("ignoring cacheFlushObserver3\n"); + return; + } + + cacheFlushObserver3 = null; + Cu.forceShrinkingGC(); + make_and_open_channel(URL, altContentType2, readAltContent3); + }, +}; + +function readAltContent3(request, buffer, closure, fromCache) { + let cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified || fromCache, true); + Assert.equal(cc.alternativeDataType, altContentType2); + Assert.equal(buffer, altContent2); + + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_alt-data_simple.js b/netwerk/test/unit/test_alt-data_simple.js new file mode 100644 index 0000000000..2132a9237f --- /dev/null +++ b/netwerk/test/unit/test_alt-data_simple.js @@ -0,0 +1,196 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we store the alt data along the channel (to the cache entry) + * - we flush the HTTP cache + * - we reload the same URL using a new channel, again prefering the alt data be loaded + * - this time the alt data must arive + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +var servedNotModified = false; +var shouldPassRevalidation = true; + +var cache_storage = null; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("ETag", "test-etag1"); + + try { + var etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + var etag = ""; + } + + if (etag == "test-etag1" && shouldPassRevalidation) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + servedNotModified = true; + } else { + var content = shouldPassRevalidation ? responseContent : responseContent2; + response.bodyOutputStream.write(content, content.length); + } +} + +function check_has_alt_data_in_index(aHasAltData, callback) { + if (inChildProcess()) { + callback(); + return; + } + + syncWithCacheIOThread(() => { + var hasAltData = {}; + cache_storage.getCacheIndexEntryAttrs(createURI(URL), "", hasAltData, {}); + Assert.equal(hasAltData.value, aHasAltData); + callback(); + }, true); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + if (!inChildProcess()) { + cache_storage = getCacheStorage("disk"); + wait_for_cache_index(asyncOpen); + } else { + asyncOpen(); + } +} + +function asyncOpen() { + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); +} + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + check_has_alt_data_in_index(false, () => { + executeSoon(() => { + var os = cc.openAlternativeOutputStream( + altContentType, + altContent.length + ); + os.write(altContent, altContent.length); + os.close(); + + executeSoon(flushAndOpenAltChannel); + }); + }); +} + +// needs to be rooted +var cacheFlushObserver = (cacheFlushObserver = { + observe() { + cacheFlushObserver = null; + openAltChannel(); + }, +}); + +function flushAndOpenAltChannel() { + // We need to do a GC pass to ensure the cache entry has been freed. + gc(); + if (!inChildProcess()) { + Services.cache2 + .QueryInterface(Ci.nsICacheTesting) + .flush(cacheFlushObserver); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(() => { + openAltChannel(); + }); + } +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType("dummy1", "text/javascript", true); + cc.preferAlternativeDataType(altContentType, "text/plain", true); + cc.preferAlternativeDataType("dummy2", "", true); + + chan.asyncOpen(new ChannelListener(readAltContent, null)); +} + +function readAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(servedNotModified, true); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(buffer, altContent); + check_has_alt_data_in_index(true, () => { + cc.getOriginalInputStream({ + onInputStreamReady(aInputStream) { + executeSoon(() => readOriginalInputStream(aInputStream)); + }, + }); + }); +} + +function readOriginalInputStream(aInputStream) { + // We expect the async stream length to match the expected content. + // If the test times out, it's probably because of this. + try { + let originalData = read_stream(aInputStream, responseContent.length); + Assert.equal(originalData, responseContent); + requestAgain(); + } catch (e) { + equal(e.result, Cr.NS_BASE_STREAM_WOULD_BLOCK); + executeSoon(() => readOriginalInputStream(aInputStream)); + } +} + +function requestAgain() { + shouldPassRevalidation = false; + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + chan.asyncOpen(new ChannelListener(readEmptyAltContent, null)); +} + +function readEmptyAltContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + // the cache is overwrite and the alt-data is reset + Assert.equal(cc.alternativeDataType, ""); + Assert.equal(buffer, responseContent2); + check_has_alt_data_in_index(false, () => httpServer.stop(do_test_finished)); +} diff --git a/netwerk/test/unit/test_alt-data_stream.js b/netwerk/test/unit/test_alt-data_stream.js new file mode 100644 index 0000000000..c6eb665226 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_stream.js @@ -0,0 +1,153 @@ +/** + * Test for the "alternative data stream" stored withing a cache entry. + * + * - we load a URL with preference for an alt data (check what we get is the raw data, + * since there was nothing previously cached) + * - we write a big chunk of alt-data to the output stream + * - we load the URL again, expecting to get alt-data + * - we check that the alt-data is streamed. We should get the first chunk, then + * the rest of the alt-data is written, and we check that it is received in + * the proper order. + * + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseContent = "response body"; +// We need a large content in order to make sure that the IPDL stream is cut +// into several different chunks. +// We fill each chunk with a different character for easy debugging. +const altContent = + "a".repeat(128 * 1024) + + "b".repeat(128 * 1024) + + "c".repeat(128 * 1024) + + "d".repeat(128 * 1024) + + "e".repeat(128 * 1024) + + "f".repeat(128 * 1024) + + "g".repeat(128 * 1024) + + "h".repeat(128 * 1024) + + "i".repeat(13); // Just so the chunk size doesn't match exactly. + +const firstChunkSize = Math.floor(altContent.length / 4); +const altContentType = "text/binary"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "max-age=86400"); + + response.bodyOutputStream.write(responseContent, responseContent.length); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL); + + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(new ChannelListener(readServerContent, null)); + do_test_pending(); +} + +// Output stream used to write alt-data to the cache entry. +var os; + +function readServerContent(request, buffer) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(buffer, responseContent); + Assert.equal(cc.alternativeDataType, ""); + + executeSoon(() => { + os = cc.openAlternativeOutputStream(altContentType, altContent.length); + // Write a quarter of the alt data content + os.write(altContent, firstChunkSize); + + executeSoon(openAltChannel); + }); +} + +function openAltChannel() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + + chan.asyncOpen(altDataListener); +} + +var altDataListener = { + buffer: "", + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.buffer += string; + + // XXX: this condition might be a bit volatile. If this test times out, + // it probably means that for some reason, the listener didn't get all the + // data in the first chunk. + if (this.buffer.length == firstChunkSize) { + // write the rest of the content + os.write( + altContent.substring(firstChunkSize, altContent.length), + altContent.length - firstChunkSize + ); + os.close(); + } + }, + onStopRequest(request, status) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(this.buffer.length, altContent.length); + Assert.equal(this.buffer, altContent); + openAltChannelWithOriginalContent(); + }, +}; + +function openAltChannelWithOriginalContent() { + var chan = make_channel(URL); + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", false); + + chan.asyncOpen(originalListener); +} + +var originalListener = { + buffer: "", + onStartRequest(request) {}, + onDataAvailable(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.buffer += string; + }, + onStopRequest(request, status) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + Assert.equal(cc.alternativeDataType, altContentType); + Assert.equal(this.buffer.length, responseContent.length); + Assert.equal(this.buffer, responseContent); + testAltDataStream(cc); + }, +}; + +function testAltDataStream(cc) { + cc.getAltDataInputStream(altContentType, { + onInputStreamReady(aInputStream) { + Assert.ok(!!aInputStream); + httpServer.stop(do_test_finished); + }, + }); +} diff --git a/netwerk/test/unit/test_alt-data_too_big.js b/netwerk/test/unit/test_alt-data_too_big.js new file mode 100644 index 0000000000..d928394272 --- /dev/null +++ b/netwerk/test/unit/test_alt-data_too_big.js @@ -0,0 +1,113 @@ +/** + * Test for handling too big alternative data + * + * - first we try to open an output stream for too big alt-data which must fail + * and leave original data intact + * + * - then we open the output stream without passing predicted data size which + * succeeds but writing must fail later at the size limit and the original + * data must be kept + */ + +"use strict"; + +var data = "data "; +var altData = "alt-data"; + +function run_test() { + do_get_profile(); + + // Expand both data to 1MB + for (let i = 0; i < 17; i++) { + data += data; + altData += altData; + } + + // Set the limit so that the data fits but alt-data doesn't. + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1800); + + write_data(); + + do_test_pending(); +} + +function write_data() { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_OK); + + var os = entry.openOutputStream(0, -1); + var written = os.write(data, data.length); + Assert.equal(written, data.length); + os.close(); + + open_big_altdata_output(entry); + } + ); +} + +function open_big_altdata_output(entry) { + try { + entry.openAlternativeOutputStream("text/binary", altData.length); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG); + } + entry.close(); + + check_entry(write_big_altdata); +} + +function write_big_altdata() { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_OK); + + var os = entry.openAlternativeOutputStream("text/binary", -1); + try { + os.write(altData, altData.length); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_FILE_TOO_BIG); + } + os.close(); + entry.close(); + + check_entry(do_test_finished); + } + ); +} + +function check_entry(cb) { + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_OK); + + var is = null; + try { + is = entry.openAlternativeInputStream("text/binary"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_NOT_AVAILABLE); + } + + is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + Assert.equal(read.length, data.length); + is.close(); + entry.close(); + + executeSoon(cb); + }); + } + ); +} diff --git a/netwerk/test/unit/test_altsvc.js b/netwerk/test/unit/test_altsvc.js new file mode 100644 index 0000000000..e9e0d5fc7b --- /dev/null +++ b/netwerk/test/unit/test_altsvc.js @@ -0,0 +1,519 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var h2Port; +var prefs; +var spdypref; +var http2pref; +var altsvcpref1; +var altsvcpref2; + +// https://foo.example.com:(h2Port) +// https://bar.example.com:(h2Port) <- invalid for bar, but ok for foo +var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort) +var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort) + +var otherServer; // server socket listening for other connection. + +var h2FooRoute; // foo.example.com:H2PORT +var h2BarRoute; // bar.example.com:H2PORT +var h2Route; // :H2PORT +var httpFooOrigin; // http://foo.exmaple.com:PORT/ +var httpsFooOrigin; // https://foo.exmaple.com:PORT/ +var httpBarOrigin; // http://bar.example.com:PORT/ +var httpsBarOrigin; // https://bar.example.com:PORT/ + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + spdypref = prefs.getBoolPref("network.http.spdy.enabled"); + http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2"); + altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled"); + altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.altsvc.enabled", true); + prefs.setBoolPref("network.http.altsvc.oe", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. The same cert is used + // for both h2FooRoute and h2BarRoute though it is only valid for + // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + h1Foo = new HttpServer(); + h1Foo.registerPathHandler("/altsvc-test", h1Server); + h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Foo.start(-1); + h1Foo.identity.setPrimary( + "http", + "foo.example.com", + h1Foo.identity.primaryPort + ); + + h1Bar = new HttpServer(); + h1Bar.registerPathHandler("/altsvc-test", h1Server); + h1Bar.start(-1); + h1Bar.identity.setPrimary( + "http", + "bar.example.com", + h1Bar.identity.primaryPort + ); + + h2FooRoute = "foo.example.com:" + h2Port; + h2BarRoute = "bar.example.com:" + h2Port; + h2Route = ":" + h2Port; + + httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/"; + httpsFooOrigin = "https://" + h2FooRoute + "/"; + httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/"; + httpsBarOrigin = "https://" + h2BarRoute + "/"; + dump( + "http foo - " + + httpFooOrigin + + "\n" + + "https foo - " + + httpsFooOrigin + + "\n" + + "http bar - " + + httpBarOrigin + + "\n" + + "https bar - " + + httpsBarOrigin + + "\n" + ); + + doTest1(); +} + +function h1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + var hval = "h2=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + var body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.spdy.enabled", spdypref); + prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.security.ports.banned"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin + "altsvc-test", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var origin; +var xaltsvc; +var retryCounter = 0; +var loadWithoutClearingMappings = false; +var disallowH3 = false; +var disallowH2 = false; +var nextTest; +var expectPass = true; +var waitFor = 0; +var originAttributes = {}; + +var Listener = function() {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + var routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.equal(Components.isSuccessCode(status), expectPass); + + if (waitFor != 0) { + Assert.equal(routed, ""); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(waitFor, doTest); + waitFor = 0; + xaltsvc = "NA"; + } else if (xaltsvc == "NA") { + Assert.equal(routed, ""); + nextTest(); + } else if (routed == xaltsvc) { + Assert.equal(routed, xaltsvc); // always true, but a useful log + nextTest(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(500, doTest); + } + + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); + do_test_pending(); + otherServer.close(); + do_test_pending(); + h1Foo.stop(do_test_finished); + do_test_pending(); + h1Bar.stop(do_test_finished); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + var chan = makeChan(origin); + var listener = new Listener(); + if (xaltsvc != "NA") { + chan.setRequestHeader("x-altsvc", xaltsvc, false); + } + if (loadWithoutClearingMappings) { + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } else { + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } + if (disallowH3) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowHttp3 = false; + disallowH3 = false; + } + if (disallowH2) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + disallowH2 = false; + } + loadWithoutClearingMappings = false; + chan.loadInfo.originAttributes = originAttributes; + chan.asyncOpen(listener); +} + +// xaltsvc is overloaded to do two things.. +// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header +// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route) +// +// When xaltsvc is set to h2Route (i.e. :port with the implied hostname) it doesn't match the alt-used, +// which is always explicit, so it needs to be changed after the channel is created but before the +// listener is invoked + +// http://foo served from h2=:port +function doTest1() { + dump("doTest1()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + nextTest = doTest2; + do_test_pending(); + doTest(); + xaltsvc = h2FooRoute; +} + +// http://foo served from h2=foo:port +function doTest2() { + dump("doTest2()\n"); + origin = httpFooOrigin; + xaltsvc = h2FooRoute; + nextTest = doTest3; + do_test_pending(); + doTest(); +} + +// http://foo served from h2=bar:port +// requires cert for foo +function doTest3() { + dump("doTest3()\n"); + origin = httpFooOrigin; + xaltsvc = h2BarRoute; + nextTest = doTest4; + do_test_pending(); + doTest(); +} + +// https://bar should fail because host bar has cert for foo +function doTest4() { + dump("doTest4()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest5; + do_test_pending(); + doTest(); +} + +// https://foo no alt-svc (just check cert setup) +function doTest5() { + dump("doTest5()\n"); + origin = httpsFooOrigin; + xaltsvc = "NA"; + expectPass = true; + nextTest = doTest6; + do_test_pending(); + doTest(); +} + +// https://foo via bar (bar has cert for foo) +function doTest6() { + dump("doTest6()\n"); + origin = httpsFooOrigin; + xaltsvc = h2BarRoute; + nextTest = doTest7; + do_test_pending(); + doTest(); +} + +// check again https://bar should fail because host bar has cert for foo +function doTest7() { + dump("doTest7()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest8; + do_test_pending(); + doTest(); +} + +// http://bar via h2 on bar +// should not use TLS/h2 because h2BarRoute is not auth'd for bar +// however the test ought to PASS (i.e. get a 200) because fallback +// to plaintext happens.. thus the timeout +function doTest8() { + dump("doTest8()\n"); + origin = httpBarOrigin; + xaltsvc = h2BarRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest9; + do_test_pending(); + doTest(); +} + +// http://bar served from h2=:port, which is like the bar route in 8 +function doTest9() { + dump("doTest9()\n"); + origin = httpBarOrigin; + xaltsvc = h2Route; + expectPass = true; + waitFor = 500; + nextTest = doTest10; + do_test_pending(); + doTest(); + xaltsvc = h2BarRoute; +} + +// check again https://bar should fail because host bar has cert for foo +function doTest10() { + dump("doTest10()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest11; + do_test_pending(); + doTest(); +} + +// http://bar served from h2=foo, should fail because host foo only has +// cert for foo. Fail in this case means alt-svc is not used, but content +// is served +function doTest11() { + dump("doTest11()\n"); + origin = httpBarOrigin; + xaltsvc = h2FooRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest12; + do_test_pending(); + doTest(); +} + +// Test 12-15: +// Insert a cache of http://foo served from h2=:port with origin attributes. +function doTest12() { + dump("doTest12()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + nextTest = doTest13; + do_test_pending(); + doTest(); + xaltsvc = h2FooRoute; +} + +// Make sure we get a cache miss with a different userContextId. +function doTest13() { + dump("doTest13()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 2, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest14; + do_test_pending(); + doTest(); +} + +// Make sure we get a cache miss with a different firstPartyDomain. +function doTest14() { + dump("doTest14()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "b.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest15; + do_test_pending(); + doTest(); +} +// +// Make sure we get a cache hit with the same origin attributes. +function doTest15() { + dump("doTest15()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest16; + do_test_pending(); + doTest(); + // This ensures a cache hit. + xaltsvc = h2FooRoute; +} + +// Make sure we do not use H2 if it is disabled on a channel. +function doTest16() { + dump("doTest16()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH2 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest17; + do_test_pending(); + doTest(); +} + +// Make sure we use H2 if only Http3 is disabled on a channel. +function doTest17() { + dump("doTest17()\n"); + origin = httpFooOrigin; + xaltsvc = h2Route; + disallowH3 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest18; + do_test_pending(); + doTest(); + // This should ensures a cache hit. + xaltsvc = h2FooRoute; +} + +// Check we don't connect to blocked ports +function doTest18() { + dump("doTest18()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + otherServer.init(-1, true, -1); + xaltsvc = "localhost:" + otherServer.port; + Services.prefs.setCharPref( + "network.security.ports.banned", + "" + otherServer.port + ); + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_altsvc_http3.js b/netwerk/test/unit/test_altsvc_http3.js new file mode 100644 index 0000000000..a443f4732f --- /dev/null +++ b/netwerk/test/unit/test_altsvc_http3.js @@ -0,0 +1,496 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var h3Port; + +// https://foo.example.com:(h3Port) +// https://bar.example.com:(h3Port) <- invalid for bar, but ok for foo +var h1Foo; // server http://foo.example.com:(h1Foo.identity.primaryPort) +var h1Bar; // server http://bar.example.com:(h1bar.identity.primaryPort) + +var otherServer; // server socket listening for other connection. + +var h3FooRoute; // foo.example.com:H3PORT +var h3BarRoute; // bar.example.com:H3PORT +var h3Route; // :H3PORT +var httpFooOrigin; // http://foo.exmaple.com:PORT/ +var httpsFooOrigin; // https://foo.exmaple.com:PORT/ +var httpBarOrigin; // http://bar.example.com:PORT/ +var httpsBarOrigin; // https://bar.example.com:PORT/ + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + // Set to allow the cert presented by our H3 server + do_get_profile(); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setBoolPref("network.http.altsvc.enabled", true); + Services.prefs.setBoolPref("network.http.altsvc.oe", true); + Services.prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. The same cert is used + // for both h3FooRoute and h3BarRoute though it is only valid for + // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + h1Foo = new HttpServer(); + h1Foo.registerPathHandler("/altsvc-test", h1Server); + h1Foo.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Foo.start(-1); + h1Foo.identity.setPrimary( + "http", + "foo.example.com", + h1Foo.identity.primaryPort + ); + + h1Bar = new HttpServer(); + h1Bar.registerPathHandler("/altsvc-test", h1Server); + h1Bar.start(-1); + h1Bar.identity.setPrimary( + "http", + "bar.example.com", + h1Bar.identity.primaryPort + ); + + h3FooRoute = "foo.example.com:" + h3Port; + h3BarRoute = "bar.example.com:" + h3Port; + h3Route = ":" + h3Port; + + httpFooOrigin = "http://foo.example.com:" + h1Foo.identity.primaryPort + "/"; + httpsFooOrigin = "https://" + h3FooRoute + "/"; + httpBarOrigin = "http://bar.example.com:" + h1Bar.identity.primaryPort + "/"; + httpsBarOrigin = "https://" + h3BarRoute + "/"; + dump( + "http foo - " + + httpFooOrigin + + "\n" + + "https foo - " + + httpsFooOrigin + + "\n" + + "http bar - " + + httpBarOrigin + + "\n" + + "https bar - " + + httpsBarOrigin + + "\n" + ); + + doTest1(); +} + +function h1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + var hval = "h3-27=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + var body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + var body = '["http://foo.example.com:' + h1Foo.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function resetPrefs() { + Services.prefs.clearUserPref("network.http.http3.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.http.altsvc.enabled"); + Services.prefs.clearUserPref("network.http.altsvc.oe"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.security.ports.banned"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin + "altsvc-test", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var origin; +var xaltsvc; +var retryCounter = 0; +var loadWithoutClearingMappings = false; +var disallowH3 = false; +var disallowH2 = false; +var testKeepAliveNotSet = false; +var nextTest; +var expectPass = true; +var waitFor = 0; +var originAttributes = {}; + +var Listener = function() {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + var routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.equal(Components.isSuccessCode(status), expectPass); + + if (waitFor != 0) { + Assert.equal(routed, ""); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(waitFor, doTest); + waitFor = 0; + xaltsvc = "NA"; + } else if (xaltsvc == "NA") { + Assert.equal(routed, ""); + nextTest(); + } else if (routed == xaltsvc) { + Assert.equal(routed, xaltsvc); // always true, but a useful log + nextTest(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + loadWithoutClearingMappings = true; + do_timeout(500, doTest); + } + + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); + do_test_pending(); + otherServer.close(); + do_test_pending(); + h1Foo.stop(do_test_finished); + do_test_pending(); + h1Bar.stop(do_test_finished); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + var chan = makeChan(origin); + var listener = new Listener(); + if (xaltsvc != "NA") { + chan.setRequestHeader("x-altsvc", xaltsvc, false); + } + if (testKeepAliveNotSet) { + chan.setRequestHeader("Connection", "close", false); + testKeepAliveNotSet = false; + } + if (loadWithoutClearingMappings) { + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } else { + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + } + if (disallowH3) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowHttp3 = false; + disallowH3 = false; + } + if (disallowH2) { + let internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + disallowH2 = false; + } + loadWithoutClearingMappings = false; + chan.loadInfo.originAttributes = originAttributes; + chan.asyncOpen(listener); +} + +// xaltsvc is overloaded to do two things.. +// 1] it is sent in the x-altsvc request header, and the response uses the value in the Alt-Svc response header +// 2] the test polls until necko sets Alt-Used to that value (i.e. it uses that route) +// +// When xaltsvc is set to h3Route (i.e. :port with the implied hostname) it doesn't match the alt-used, +// which is always explicit, so it needs to be changed after the channel is created but before the +// listener is invoked + +// http://foo served from h3-27=:port +function doTest1() { + dump("doTest1()\n"); + origin = httpFooOrigin; + xaltsvc = h3Route; + nextTest = doTest2; + do_test_pending(); + doTest(); + xaltsvc = h3FooRoute; +} + +// http://foo served from h3-27=foo:port +function doTest2() { + dump("doTest2()\n"); + origin = httpFooOrigin; + xaltsvc = h3FooRoute; + nextTest = doTest3; + do_test_pending(); + doTest(); +} + +// http://foo served from h3-27=bar:port +// requires cert for foo +function doTest3() { + dump("doTest3()\n"); + origin = httpFooOrigin; + xaltsvc = h3BarRoute; + nextTest = doTest4; + do_test_pending(); + doTest(); +} + +// https://bar should fail because host bar has cert for foo +function doTest4() { + dump("doTest4()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest5; + do_test_pending(); + doTest(); +} + +// http://bar via h3 on bar +// should not use TLS/h3 because h3BarRoute is not auth'd for bar +// however the test ought to PASS (i.e. get a 200) because fallback +// to plaintext happens.. thus the timeout +function doTest5() { + dump("doTest5()\n"); + origin = httpBarOrigin; + xaltsvc = h3BarRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest6; + do_test_pending(); + doTest(); +} + +// http://bar served from h3-27=:port, which is like the bar route in 8 +function doTest6() { + dump("doTest6()\n"); + origin = httpBarOrigin; + xaltsvc = h3Route; + expectPass = true; + waitFor = 500; + nextTest = doTest7; + do_test_pending(); + doTest(); + xaltsvc = h3BarRoute; +} + +// check again https://bar should fail because host bar has cert for foo +function doTest7() { + dump("doTest7()\n"); + origin = httpsBarOrigin; + xaltsvc = ""; + expectPass = false; + nextTest = doTest8; + do_test_pending(); + doTest(); +} + +// http://bar served from h3-27=foo, should fail because host foo only has +// cert for foo. Fail in this case means alt-svc is not used, but content +// is served +function doTest8() { + dump("doTest8()\n"); + origin = httpBarOrigin; + xaltsvc = h3FooRoute; + expectPass = true; + waitFor = 500; + nextTest = doTest9; + do_test_pending(); + doTest(); +} + +// Test 9-12: +// Insert a cache of http://foo served from h3-27=:port with origin attributes. +function doTest9() { + dump("doTest9()\n"); + origin = httpFooOrigin; + xaltsvc = h3Route; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + nextTest = doTest10; + do_test_pending(); + doTest(); + xaltsvc = h3FooRoute; +} + +// Make sure we get a cache miss with a different userContextId. +function doTest10() { + dump("doTest10()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 2, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest11; + do_test_pending(); + doTest(); +} + +// Make sure we get a cache miss with a different firstPartyDomain. +function doTest11() { + dump("doTest11()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "b.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest12; + do_test_pending(); + doTest(); +} +// +// Make sure we get a cache hit with the same origin attributes. +function doTest12() { + dump("doTest12()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest13; + do_test_pending(); + doTest(); + // This ensures a cache hit. + xaltsvc = h3FooRoute; +} + +// Make sure we do not use H3 if it is disabled on a channel. +function doTest13() { + dump("doTest13()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH3 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest14; + do_test_pending(); + doTest(); +} + +// Make sure we use H3 if only Http2 is disabled on a channel. +function doTest14() { + dump("doTest14()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + disallowH2 = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest15; + do_test_pending(); + doTest(); + // This should ensures a cache hit. + xaltsvc = h3FooRoute; +} + +// Make sure we do not use H3 if NS_HTTP_ALLOW_KEEPALIVE is not set. +function doTest15() { + dump("doTest15()\n"); + origin = httpFooOrigin; + xaltsvc = "NA"; + testKeepAliveNotSet = true; + originAttributes = { + userContextId: 1, + firstPartyDomain: "a.com", + }; + loadWithoutClearingMappings = true; + nextTest = doTest16; + do_test_pending(); + doTest(); +} + +// Check we don't connect to blocked ports +function doTest16() { + dump("doTest16()\n"); + origin = httpFooOrigin; + nextTest = testsDone; + otherServer = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + otherServer.init(-1, true, -1); + xaltsvc = "localhost:" + otherServer.port; + Services.prefs.setCharPref( + "network.security.ports.banned", + "" + otherServer.port + ); + dump("Blocked port: " + otherServer.port); + waitFor = 500; + otherServer.asyncListen({ + onSocketAccepted() { + Assert.ok(false, "Got connection to socket when we didn't expect it!"); + }, + onStopListening() { + // We get closed when the entire file is done, which guarantees we get the socket accept + // if we do connect to the alt-svc header + do_test_finished(); + }, + }); + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_altsvc_pref.js b/netwerk/test/unit/test_altsvc_pref.js new file mode 100644 index 0000000000..a93d67dac5 --- /dev/null +++ b/netwerk/test/unit/test_altsvc_pref.js @@ -0,0 +1,139 @@ +"use strict"; + +let h3Port; +let h3Route; +let h3AltSvc; +let prefs; +let httpsOrigin; + +let tests = [ + // The altSvc storage may not be up imediately, therefore run test_no_altsvc_pref + // for a couple times to wait for the storage. + test_no_altsvc_pref, + test_no_altsvc_pref, + test_no_altsvc_pref, + test_altsvc_pref, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.http3.enabled", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + prefs.setBoolPref("network.dns.disableIPv6", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com/"; + + run_next_test(); +} + +let Http3CheckListener = function() {}; + +Http3CheckListener.prototype = { + expectedRoute: "", + expectedStatus: Cr.NS_OK, + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + } + + do_test_finished(); + }, +}; + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function test_no_altsvc_pref() { + dump("test_no_altsvc_pref"); + do_test_pending(); + + let chan = makeChan(httpsOrigin + "http3-test"); + let listener = new Http3CheckListener(); + listener.expectedStatus = Cr.NS_ERROR_CONNECTION_REFUSED; + chan.asyncOpen(listener); +} + +function test_altsvc_pref() { + dump("test_altsvc_pref"); + do_test_pending(); + + prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=" + h3AltSvc + ); + + let chan = makeChan(httpsOrigin + "http3-test"); + let listener = new Http3CheckListener(); + listener.expectedRoute = h3Route; + chan.asyncOpen(listener); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.disableIPv6"); + prefs.clearUserPref("network.http.http3.alt-svc-mapping-for-testing"); + dump("testDone\n"); +} diff --git a/netwerk/test/unit/test_anonymous-coalescing.js b/netwerk/test/unit/test_anonymous-coalescing.js new file mode 100644 index 0000000000..e118f6f96c --- /dev/null +++ b/netwerk/test/unit/test_anonymous-coalescing.js @@ -0,0 +1,186 @@ +/* +- test to check we use only a single connection for both onymous and anonymous requests over an existing h2 session +- request from a domain w/o LOAD_ANONYMOUS flag +- request again from the same domain, but different URI, with LOAD_ANONYMOUS flag, check the client is using the same conn +- close all and do it in the opposite way (do an anonymous req first) +*/ + +"use strict"; + +var h2Port; +var prefs; +var spdypref; +var http2pref; +var extpref; + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + spdypref = prefs.getBoolPref("network.http.spdy.enabled"); + http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2"); + extpref = prefs.getBoolPref("network.http.originextension"); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.originextension", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, alt1.example.com" + ); + + // The moz-http2 cert is for {foo, alt1, alt2}.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + doTest1(); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.spdy.enabled", spdypref); + prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref); + prefs.setBoolPref("network.http.originextension", extpref); + prefs.clearUserPref("network.dns.localDomains"); +} + +function makeChan(origin) { + return NetUtil.newChannel({ + uri: origin, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var nextTest; +var origin; +var nextPortExpectedToBeSame = false; +var currentPort = 0; +var forceReload = false; +var anonymous = false; + +var Listener = function() {}; +Listener.prototype.clientPort = 0; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + Assert.equal(request.responseStatus, 200); + this.clientPort = parseInt(request.getResponseHeader("x-client-port")); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(Components.isSuccessCode(status)); + if (nextPortExpectedToBeSame) { + Assert.equal(currentPort, this.clientPort); + } else { + Assert.notEqual(currentPort, this.clientPort); + } + currentPort = this.clientPort; + nextTest(); + do_test_finished(); + }, +}; + +function testsDone() { + dump("testsDone\n"); + resetPrefs(); +} + +function doTest() { + dump("execute doTest " + origin + "\n"); + + var loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + if (anonymous) { + loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS; + } + anonymous = false; + if (forceReload) { + loadFlags |= Ci.nsIRequest.LOAD_FRESH_CONNECTION; + } + forceReload = false; + + var chan = makeChan(origin); + chan.loadFlags = loadFlags; + + var listener = new Listener(); + chan.asyncOpen(listener); +} + +function doTest1() { + dump("doTest1()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-1"; + nextTest = doTest2; + nextPortExpectedToBeSame = false; + do_test_pending(); + doTest(); +} + +function doTest2() { + // Run the same test as above to make sure connection is marked experienced. + dump("doTest2()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-1"; + nextTest = doTest3; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} + +function doTest3() { + // connection expected to be reused for an anonymous request + dump("doTest3()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-2"; + nextTest = doTest4; + nextPortExpectedToBeSame = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest4() { + dump("doTest4()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-3"; + nextTest = doTest5; + nextPortExpectedToBeSame = false; + forceReload = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest5() { + // Run the same test as above just without forceReload to make sure connection + // is marked experienced. + dump("doTest5()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-3"; + nextTest = doTest6; + nextPortExpectedToBeSame = true; + anonymous = true; + do_test_pending(); + doTest(); +} + +function doTest6() { + dump("doTest6()\n"); + origin = "https://foo.example.com:" + h2Port + "/origin-4"; + nextTest = testsDone; + nextPortExpectedToBeSame = true; + do_test_pending(); + doTest(); +} diff --git a/netwerk/test/unit/test_auth_dialog_permission.js b/netwerk/test/unit/test_auth_dialog_permission.js new file mode 100644 index 0000000000..18a9c8286b --- /dev/null +++ b/netwerk/test/unit/test_auth_dialog_permission.js @@ -0,0 +1,283 @@ +// This file tests authentication prompt depending on pref +// network.auth.subresource-http-auth-allow: +// 0 - don't allow sub-resources to open HTTP authentication credentials +// dialogs +// 1 - allow sub-resources to open HTTP authentication credentials dialogs, +// but don't allow it for cross-origin sub-resources +// 2 - allow the cross-origin authentication as well. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +// Since this test creates a TYPE_DOCUMENT channel via javascript, it will +// end up using the wrong LoadInfo constructor. Setting this pref will disable +// the ContentPolicyType assertion in the constructor. +prefs.setBoolPref("network.loadinfo.skip_type_assertion", true); + +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +var httpserv = new HttpServer(); +httpserv.registerPathHandler("/auth", authHandler); +httpserv.start(-1); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PORT", function() { + return httpserv.identity.primaryPort; +}); + +function AuthPrompt(promptExpected) { + this.promptExpected = promptExpected; +} + +AuthPrompt.prototype = { + user: "guest", + pass: "guest", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword(title, text, realm, savePW, user, pw) { + Assert.ok(this.promptExpected, "Not expected the authentication prompt."); + + user.value = this.user; + pw.value = this.pass; + return true; + }, + + promptPassword(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function Requestor(promptExpected) { + this.promptExpected = promptExpected; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + this.prompter = new AuthPrompt(this.promptExpected); + return this.prompter; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompter: null, +}; + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +function makeChan(loadingUrl, url, contentPolicy) { + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri(loadingUrl); + var principal = ssm.createContentPrincipal(uri, {}); + + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT, + contentPolicyType: contentPolicy, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function Test( + subresource_http_auth_allow_pref, + loadingUri, + uri, + contentPolicy, + expectedCode +) { + this._subresource_http_auth_allow_pref = subresource_http_auth_allow_pref; + this._loadingUri = loadingUri; + this._uri = uri; + this._contentPolicy = contentPolicy; + this._expectedCode = expectedCode; +} + +Test.prototype = { + _subresource_http_auth_allow_pref: 1, + _loadingUri: null, + _uri: null, + _contentPolicy: Ci.nsIContentPolicy.TYPE_OTHER, + _expectedCode: 200, + + onStartRequest(request) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(request.responseStatus, this._expectedCode); + // The request should be succeeded iff we expect 200 + Assert.equal(request.requestSucceeded, this._expectedCode == 200); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable(request, stream, offset, count) { + do_throw("Should not get any data!"); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + // Clear the auth cache. + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + do_timeout(0, run_next_test); + }, + + run() { + dump( + "Run test: " + + this._subresource_http_auth_allow_pref + + this._loadingUri + + this._uri + + this._contentPolicy + + this._expectedCode + + " \n" + ); + + prefs.setIntPref( + "network.auth.subresource-http-auth-allow", + this._subresource_http_auth_allow_pref + ); + let chan = makeChan(this._loadingUri, this._uri, this._contentPolicy); + chan.notificationCallbacks = new Requestor(this._expectedCode == 200); + chan.asyncOpen(this); + }, +}; + +var tests = [ + // For the next 3 tests the preference is set to 2 - allow the cross-origin + // authentication as well. + + // A cross-origin request. + new Test( + 2, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 200 + ), + // A non cross-origin sub-resource request. + new Test(2, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200), + // A top level document. + new Test( + 2, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), + + // For the next 3 tests the preference is set to 1 - allow sub-resources to + // open HTTP authentication credentials dialogs, but don't allow it for + // cross-origin sub-resources + + // A cross-origin request. + new Test( + 1, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 401 + ), + // A non cross-origin sub-resource request. + new Test(1, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 200), + // A top level document. + new Test( + 1, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), + + // For the next 3 tests the preference is set to 0 - don't allow sub-resources + // to open HTTP authentication credentials dialogs. + + // A cross-origin request. + new Test( + 0, + "http://example.com", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_OTHER, + 401 + ), + // A sub-resource request. + new Test(0, URL + "/", URL + "/auth", Ci.nsIContentPolicy.TYPE_OTHER, 401), + // A top level request. + new Test( + 0, + URL + "/auth", + URL + "/auth", + Ci.nsIContentPolicy.TYPE_DOCUMENT, + 200 + ), +]; + +function run_next_test() { + var nextTest = tests.shift(); + if (!nextTest) { + httpserv.stop(do_test_finished); + return; + } + + nextTest.run(); +} + +function run_test() { + do_test_pending(); + run_next_test(); +} diff --git a/netwerk/test/unit/test_auth_jar.js b/netwerk/test/unit/test_auth_jar.js new file mode 100644 index 0000000000..75c0dd2c82 --- /dev/null +++ b/netwerk/test/unit/test_auth_jar.js @@ -0,0 +1,97 @@ +"use strict"; + +function createURI(s) { + let service = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + return service.newURI(s); +} + +function run_test() { + // Set up a profile. + do_get_profile(); + + var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + const kURI1 = "http://example.com"; + var app = secMan.createContentPrincipal(createURI(kURI1), {}); + var appbrowser = secMan.createContentPrincipal(createURI(kURI1), { + inIsolatedMozBrowser: true, + }); + + var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + am.setAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + "example.com", + "user", + "pass", + false, + app + ); + am.setAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + "example.com", + "user3", + "pass3", + false, + appbrowser + ); + + Services.clearData.deleteDataFromOriginAttributesPattern({ + inIsolatedMozBrowser: true, + }); + + var domain = { value: "" }, + user = { value: "" }, + pass = { value: "" }; + try { + am.getAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + domain, + user, + pass, + false, + appbrowser + ); + Assert.equal(false, true); // no identity should be present + } catch (x) { + Assert.equal(domain.value, ""); + Assert.equal(user.value, ""); + Assert.equal(pass.value, ""); + } + + am.getAuthIdentity( + "http", + "a.example.com", + -1, + "basic", + "realm", + "", + domain, + user, + pass, + false, + app + ); + Assert.equal(domain.value, "example.com"); + Assert.equal(user.value, "user"); + Assert.equal(pass.value, "pass"); +} diff --git a/netwerk/test/unit/test_auth_proxy.js b/netwerk/test/unit/test_auth_proxy.js new file mode 100644 index 0000000000..ab0dee0bdb --- /dev/null +++ b/netwerk/test/unit/test_auth_proxy.js @@ -0,0 +1,465 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This tests the automatic login to the proxy with password, + * if the password is stored and the browser is restarted. + * + * + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_PREVIOUS_FAILED = 1 << 2; + +function AuthPrompt2(proxyFlags, hostFlags) { + this.proxyCred.flags = proxyFlags; + this.hostCred.flags = hostFlags; +} +AuthPrompt2.prototype = { + proxyCred: { + user: "proxy", + pass: "guest", + realmExpected: "intern", + flags: 0, + }, + hostCred: { user: "host", pass: "guest", realmExpected: "extern", flags: 0 }, + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, encryptionLevel, authInfo) { + try { + // never HOST and PROXY set at the same time in prompt + Assert.equal( + (authInfo.flags & Ci.nsIAuthInformation.AUTH_HOST) != 0, + (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) == 0 + ); + + var isProxy = (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) != 0; + var cred = isProxy ? this.proxyCred : this.hostCred; + + dump( + "with flags: " + + ((cred.flags & FLAG_WRONG_PASSWORD) != 0 ? "wrong password" : "") + + " " + + ((cred.flags & FLAG_PREVIOUS_FAILED) != 0 ? "previous failed" : "") + + " " + + ((cred.flags & FLAG_RETURN_FALSE) != 0 ? "return false" : "") + + "\n" + ); + + // PROXY properly set by necko (checked using realm) + Assert.equal(cred.realmExpected, authInfo.realm); + + // PREVIOUS_FAILED properly set by necko + Assert.equal( + (cred.flags & FLAG_PREVIOUS_FAILED) != 0, + (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) != 0 + ); + + if (cred.flags & FLAG_RETURN_FALSE) { + cred.flags |= FLAG_PREVIOUS_FAILED; + cred.flags &= ~FLAG_RETURN_FALSE; + return false; + } + + authInfo.username = cred.user; + if (cred.flags & FLAG_WRONG_PASSWORD) { + authInfo.password = cred.pass + ".wrong"; + cred.flags |= FLAG_PREVIOUS_FAILED; + // Now clear the flag to avoid an infinite loop + cred.flags &= ~FLAG_WRONG_PASSWORD; + } else { + authInfo.password = cred.pass; + cred.flags &= ~FLAG_PREVIOUS_FAILED; + } + return true; + } catch (e) { + do_throw(e); + } + }, + + asyncPromptAuth: function ap2_async( + channel, + callback, + context, + encryptionLevel, + authInfo + ) { + try { + var me = this; + var allOverAndDead = false; + executeSoon(function() { + try { + if (allOverAndDead) { + throw "already canceled"; + } + var ret = me.promptAuth(channel, encryptionLevel, authInfo); + if (!ret) { + callback.onAuthCancelled(context, true); + } else { + callback.onAuthAvailable(context, authInfo); + } + allOverAndDead = true; + } catch (e) { + do_throw(e); + } + }); + return new Cancelable(function() { + if (allOverAndDead) { + throw "can't cancel, already ran"; + } + callback.onAuthAvailable(context, authInfo); + allOverAndDead = true; + }); + } catch (e) { + do_throw(e); + } + }, +}; + +function Cancelable(onCancelFunc) { + this.onCancelFunc = onCancelFunc; +} +Cancelable.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), + cancel: function cancel() { + try { + this.onCancelFunc(); + } catch (e) { + do_throw(e); + } + }, +}; + +function Requestor(proxyFlags, hostFlags) { + this.proxyFlags = proxyFlags; + this.hostFlags = hostFlags; +} +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (iid.equals(Ci.nsIAuthPrompt)) { + dump("authprompt1 not implemented\n"); + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + if (iid.equals(Ci.nsIAuthPrompt2)) { + try { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.proxyFlags, this.hostFlags); + } + return this.prompt2; + } catch (e) { + do_throw(e); + } + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt2: null, +}; + +var listener = { + expectedCode: -1, // uninitialized + + onStartRequest: function test_onStartR(request) { + try { + // Proxy auth cancellation return failures to avoid spoofing + if ( + !Components.isSuccessCode(request.status) && + this.expectedCode != 407 + ) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(this.expectedCode, request.responseStatus); + // If we expect 200, the request should have succeeded + Assert.equal(this.expectedCode == 200, request.requestSucceeded); + + var cookie = ""; + try { + cookie = request.getRequestHeader("Cookie"); + } catch (e) {} + Assert.equal(cookie, ""); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + if (current_test < tests.length - 1) { + // First, need to clear the auth cache + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + + do_test_finished(); + }, +}; + +function makeChan(url) { + if (!url) { + url = "http://somesite/"; + } + + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var current_test = 0; +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", proxyAuthHandler); + httpserv.identity.add("http", "somesite", 80); + httpserv.start(-1); + + Services.prefs.setCharPref("network.proxy.http", "localhost"); + Services.prefs.setIntPref( + "network.proxy.http_port", + httpserv.identity.primaryPort + ); + Services.prefs.setCharPref("network.proxy.no_proxies_on", ""); + Services.prefs.setIntPref("network.proxy.type", 1); + + // Turn off the authentication dialog blocking for this test. + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + Services.prefs.setBoolPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow", + true + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.proxy.http"); + Services.prefs.clearUserPref("network.proxy.http_port"); + Services.prefs.clearUserPref("network.proxy.no_proxies_on"); + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.auth.subresource-http-auth-allow"); + Services.prefs.clearUserPref( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ); + }); + + tests[current_test](); +} + +function test_proxy_returnfalse() { + dump("\ntest: proxy returnfalse\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0); + listener.expectedCode = 407; // Proxy Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_proxy_wrongpw() { + dump("\ntest: proxy wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 0); + listener.expectedCode = 200; // Eventually OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_all_ok() { + dump("\ntest: all ok\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, 0); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_407_cookie() { + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 0); + chan.setRequestHeader("X-Set-407-Cookie", "1", false); + listener.expectedCode = 407; // Proxy Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_proxy_200_cookie() { + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, 0); + chan.setRequestHeader("X-Set-407-Cookie", "1", false); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_host_returnfalse() { + dump("\ntest: host returnfalse\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, FLAG_RETURN_FALSE); + listener.expectedCode = 401; // Host Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_host_wrongpw() { + dump("\ntest: host wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor(0, FLAG_WRONG_PASSWORD); + listener.expectedCode = 200; // Eventually OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_wrongpw_host_wrongpw() { + dump("\ntest: proxy wrongpw, host wrongpw\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor( + FLAG_WRONG_PASSWORD, + FLAG_WRONG_PASSWORD + ); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + do_test_pending(); +} + +function test_proxy_wrongpw_host_returnfalse() { + dump("\ntest: proxy wrongpw, host return false\n"); + var chan = makeChan(); + chan.notificationCallbacks = new Requestor( + FLAG_WRONG_PASSWORD, + FLAG_RETURN_FALSE + ); + listener.expectedCode = 401; // Host Unauthorized + chan.asyncOpen(listener); + do_test_pending(); +} + +var tests = [ + test_proxy_returnfalse, + test_proxy_wrongpw, + test_all_ok, + test_proxy_407_cookie, + test_proxy_200_cookie, + test_host_returnfalse, + test_host_wrongpw, + test_proxy_wrongpw_host_wrongpw, + test_proxy_wrongpw_host_returnfalse, +]; + +// PATH HANDLERS + +// Proxy +function proxyAuthHandler(metadata, response) { + try { + var realm = "intern"; + // btoa("proxy:guest"), but that function is not available here + var expectedHeader = "Basic cHJveHk6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Proxy-Authorization") && + metadata.getHeader("Proxy-Authorization") == expectedHeader + ) { + dump("proxy password ok\n"); + response.setHeader( + "Proxy-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + + hostAuthHandler(metadata, response); + } else { + dump("proxy password required\n"); + response.setStatusLine( + metadata.httpVersion, + 407, + "Unauthorized by HTTP proxy" + ); + response.setHeader( + "Proxy-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + if (metadata.hasHeader("X-Set-407-Cookie")) { + response.setHeader("Set-Cookie", "chewy", false); + } + body = "failed"; + response.bodyOutputStream.write(body, body.length); + } + } catch (e) { + do_throw(e); + } +} + +// Host /auth +function hostAuthHandler(metadata, response) { + try { + var realm = "extern"; + // btoa("host:guest"), but that function is not available here + var expectedHeader = "Basic aG9zdDpndWVzdA=="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + dump("host password ok\n"); + response.setStatusLine( + metadata.httpVersion, + 200, + "OK, authorized for host" + ); + response.setHeader( + "WWW-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + body = "success"; + } else { + dump("host password required\n"); + response.setStatusLine( + metadata.httpVersion, + 401, + "Unauthorized by HTTP server host" + ); + response.setHeader( + "WWW-Authenticate", + 'Basic realm="' + realm + '"', + false + ); + body = "failed"; + } + response.bodyOutputStream.write(body, body.length); + } catch (e) { + do_throw(e); + } +} diff --git a/netwerk/test/unit/test_authentication.js b/netwerk/test/unit/test_authentication.js new file mode 100644 index 0000000000..b49b0569b9 --- /dev/null +++ b/netwerk/test/unit/test_authentication.js @@ -0,0 +1,754 @@ +// This file tests authentication prompt callbacks +// TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Turn off the authentication dialog blocking for this test. +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); +prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PORT", function() { + return httpserv.identity.primaryPort; +}); + +const FLAG_RETURN_FALSE = 1 << 0; +const FLAG_WRONG_PASSWORD = 1 << 1; +const FLAG_BOGUS_USER = 1 << 2; +const FLAG_PREVIOUS_FAILED = 1 << 3; +const CROSS_ORIGIN = 1 << 4; +const FLAG_NO_REALM = 1 << 5; +const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6; + +const nsIAuthPrompt2 = Ci.nsIAuthPrompt2; +const nsIAuthInformation = Ci.nsIAuthInformation; + +function AuthPrompt1(flags) { + this.flags = flags; +} + +AuthPrompt1.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + do_throw("unexpected prompt call"); + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + if (this.flags & FLAG_NO_REALM) { + // Note that the realm here isn't actually the realm. it's a pw mgr key. + Assert.equal(URL + " (" + this.expectedRealm + ")", realm); + } + if (!(this.flags & CROSS_ORIGIN)) { + if (!text.includes(this.expectedRealm)) { + do_throw("Text must indicate the realm"); + } + } else if (text.includes(this.expectedRealm)) { + do_throw("There should not be realm for cross origin"); + } + if (!text.includes("localhost")) { + do_throw("Text must indicate the hostname"); + } + if (!text.includes(String(PORT))) { + do_throw("Text must indicate the port"); + } + if (text.includes("-1")) { + do_throw("Text must contain negative numbers"); + } + + if (this.flags & FLAG_RETURN_FALSE) { + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + user.value = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + pw.value = this.pass + ".wrong"; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + pw.value = "é"; + } else { + pw.value = this.pass; + } + return true; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + do_throw("unexpected promptPassword call"); + }, +}; + +function AuthPrompt2(flags) { + this.flags = flags; +} + +AuthPrompt2.prototype = { + user: "guest", + pass: "guest", + + expectedRealm: "secret", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function ap2_promptAuth(channel, level, authInfo) { + var isNTLM = channel.URI.pathQueryRef.includes("ntlm"); + var isDigest = channel.URI.pathQueryRef.includes("digest"); + + if (isNTLM || this.flags & FLAG_NO_REALM) { + this.expectedRealm = ""; // NTLM knows no realms + } + + Assert.equal(this.expectedRealm, authInfo.realm); + + var expectedLevel = + isNTLM || isDigest + ? nsIAuthPrompt2.LEVEL_PW_ENCRYPTED + : nsIAuthPrompt2.LEVEL_NONE; + Assert.equal(expectedLevel, level); + + var expectedFlags = nsIAuthInformation.AUTH_HOST; + + if (this.flags & FLAG_PREVIOUS_FAILED) { + expectedFlags |= nsIAuthInformation.PREVIOUS_FAILED; + } + + if (this.flags & CROSS_ORIGIN) { + expectedFlags |= nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + } + + if (isNTLM) { + expectedFlags |= nsIAuthInformation.NEED_DOMAIN; + } + + const kAllKnownFlags = 127; // Don't fail test for newly added flags + Assert.equal(expectedFlags, authInfo.flags & kAllKnownFlags); + + var expectedScheme = isNTLM ? "ntlm" : isDigest ? "digest" : "basic"; + Assert.equal(expectedScheme, authInfo.authenticationScheme); + + // No passwords in the URL -> nothing should be prefilled + Assert.equal(authInfo.username, ""); + Assert.equal(authInfo.password, ""); + Assert.equal(authInfo.domain, ""); + + if (this.flags & FLAG_RETURN_FALSE) { + this.flags |= FLAG_PREVIOUS_FAILED; + return false; + } + + if (this.flags & FLAG_BOGUS_USER) { + this.user = "foo\nbar"; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + this.user = "é"; + } + + authInfo.username = this.user; + if (this.flags & FLAG_WRONG_PASSWORD) { + authInfo.password = this.pass + ".wrong"; + this.flags |= FLAG_PREVIOUS_FAILED; + // Now clear the flag to avoid an infinite loop + this.flags &= ~FLAG_WRONG_PASSWORD; + } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) { + authInfo.password = "é"; + } else { + authInfo.password = this.pass; + this.flags &= ~FLAG_PREVIOUS_FAILED; + } + return true; + }, + + asyncPromptAuth: function ap2_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +function Requestor(flags, versions) { + this.flags = flags; + this.versions = versions; +} + +Requestor.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), + + getInterface: function requestor_gi(iid) { + if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) { + // Allow the prompt to store state by caching it here + if (!this.prompt1) { + this.prompt1 = new AuthPrompt1(this.flags); + } + return this.prompt1; + } + if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) { + // Allow the prompt to store state by caching it here + if (!this.prompt2) { + this.prompt2 = new AuthPrompt2(this.flags); + } + return this.prompt2; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + prompt1: null, + prompt2: null, +}; + +function RealmTestRequestor() {} + +RealmTestRequestor.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + ]), + + getInterface: function realmtest_interface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + promptAuth: function realmtest_checkAuth(channel, level, authInfo) { + Assert.equal(authInfo.realm, '"foo_bar'); + + return false; + }, + + asyncPromptAuth: function realmtest_async(chan, cb, ctx, lvl, info) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; + +var listener = { + expectedCode: -1, // Uninitialized + + onStartRequest: function test_onStartR(request) { + try { + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code!"); + } + + if (!(request instanceof Ci.nsIHttpChannel)) { + do_throw("Expecting an HTTP channel"); + } + + Assert.equal(request.responseStatus, this.expectedCode); + // The request should be succeeded iff we expect 200 + Assert.equal(request.requestSucceeded, this.expectedCode == 200); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + Assert.equal(status, Cr.NS_ERROR_ABORT); + + moveToNextTest(); + }, +}; + +function makeChan(url, loadingUrl) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var principal = ssm.createContentPrincipal(ios.newURI(loadingUrl), {}); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +var tests = [ + test_noauth, + test_returnfalse1, + test_wrongpw1, + test_prompt1, + test_prompt1CrossOrigin, + test_prompt2CrossOrigin, + test_returnfalse2, + test_wrongpw2, + test_prompt2, + test_ntlm, + test_basicrealm, + test_nonascii, + test_digest_noauth, + test_digest, + test_digest_bogus_user, + test_short_digest, + test_large_realm, + test_large_domain, + test_nonascii_xhr, +]; + +var current_test = 0; + +var httpserv = null; + +function moveToNextTest() { + if (current_test < tests.length - 1) { + // First, gotta clear the auth cache + Cc["@mozilla.org/network/http-auth-manager;1"] + .getService(Ci.nsIHttpAuthManager) + .clearAll(); + + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + + do_test_finished(); +} + +function run_test() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/auth", authHandler); + httpserv.registerPathHandler("/auth/ntlm/simple", authNtlmSimple); + httpserv.registerPathHandler("/auth/realm", authRealm); + httpserv.registerPathHandler("/auth/non_ascii", authNonascii); + httpserv.registerPathHandler("/auth/digest", authDigest); + httpserv.registerPathHandler("/auth/short_digest", authShortDigest); + httpserv.registerPathHandler("/largeRealm", largeRealm); + httpserv.registerPathHandler("/largeDomain", largeDomain); + + httpserv.start(-1); + + tests[0](); +} + +function test_noauth() { + var chan = makeChan(URL + "/auth", URL); + + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_returnfalse1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 1); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_wrongpw1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 1); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_prompt1() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 1); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_prompt1CrossOrigin() { + var chan = makeChan(URL + "/auth", "http://example.org"); + + chan.notificationCallbacks = new Requestor(16, 1); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_prompt2CrossOrigin() { + var chan = makeChan(URL + "/auth", "http://example.org"); + + chan.notificationCallbacks = new Requestor(16, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_returnfalse2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_wrongpw2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(FLAG_WRONG_PASSWORD, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_prompt2() { + var chan = makeChan(URL + "/auth", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_ntlm() { + var chan = makeChan(URL + "/auth/ntlm/simple", URL); + + chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_basicrealm() { + var chan = makeChan(URL + "/auth/realm", URL); + + chan.notificationCallbacks = new RealmTestRequestor(); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_nonascii() { + var chan = makeChan(URL + "/auth/non_ascii", URL); + + chan.notificationCallbacks = new Requestor(FLAG_NON_ASCII_USER_PASSWORD, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_nonascii_xhr() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", URL + "/auth/non_ascii", true, "é", "é"); + xhr.onreadystatechange = function(event) { + if (xhr.readyState == 4) { + Assert.equal(xhr.status, 200); + moveToNextTest(); + xhr.onreadystatechange = null; + } + }; + xhr.send(null); + + do_test_pending(); +} + +function test_digest_noauth() { + var chan = makeChan(URL + "/auth/digest", URL); + + //chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2); + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_digest() { + var chan = makeChan(URL + "/auth/digest", URL); + + chan.notificationCallbacks = new Requestor(0, 2); + listener.expectedCode = 200; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_digest_bogus_user() { + var chan = makeChan(URL + "/auth/digest", URL); + chan.notificationCallbacks = new Requestor(FLAG_BOGUS_USER, 2); + listener.expectedCode = 401; // unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +// Test header "WWW-Authenticate: Digest" - bug 1338876. +function test_short_digest() { + var chan = makeChan(URL + "/auth/short_digest", URL); + chan.notificationCallbacks = new Requestor(FLAG_NO_REALM, 2); + listener.expectedCode = 401; // OK + chan.asyncOpen(listener); + + do_test_pending(); +} + +// PATH HANDLERS + +// /auth +function authHandler(metadata, response) { + // btoa("guest:guest"), but that function is not available here + var expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "success"; + } else { + // didn't know guest:guest, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/ntlm/simple +function authNtlmSimple(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + "NTLM" /* + ' realm="secret"' */, + false + ); + + var body = + "NOTE: This just sends an NTLM challenge, it never\n" + + "accepts the authentication. It also closes\n" + + "the connection after sending the challenge\n"; + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/realm +function authRealm(metadata, response) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="\\"f\\oo_bar"', false); + var body = "success"; + + response.bodyOutputStream.write(body, body.length); +} + +// /auth/nonAscii +function authNonascii(metadata, response) { + // btoa("é:é"), but that function is not available here + var expectedHeader = "Basic w6k6w6k="; + + var body; + if ( + metadata.hasHeader("Authorization") && + metadata.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + // Use correct XML syntax since this function is also used for testing XHR. + body = "success"; + } else { + // didn't know é:é, failure + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + + body = "failed"; + } + + response.bodyOutputStream.write(body, body.length); +} + +// +// Digest functions +// +function bytesFromString(str) { + var converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + var data = converter.convertToByteArray(str); + return data; +} + +// return the two-digit hexadecimal code for a byte +function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); +} + +function H(str) { + var data = bytesFromString(str); + var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(Ci.nsICryptoHash.MD5); + ch.update(data, data.length); + var hash = ch.finish(false); + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +// +// Digest handler +// +// /auth/digest +function authDigest(metadata, response) { + var nonce = "6f93719059cf8d568005727f3250e798"; + var opaque = "1234opaque1234"; + var cnonceRE = /cnonce="(\w+)"/; + var responseRE = /response="(\w+)"/; + var usernameRE = /username="(\w+)"/; + var authenticate = + 'Digest realm="secret", domain="/", qop=auth,' + + 'algorithm=MD5, nonce="' + + nonce + + '" opaque="' + + opaque + + '"'; + var body; + // check creds if we have them + if (metadata.hasHeader("Authorization")) { + var auth = metadata.getHeader("Authorization"); + var cnonce = auth.match(cnonceRE)[1]; + var clientDigest = auth.match(responseRE)[1]; + var username = auth.match(usernameRE)[1]; + var nc = "00000001"; + + if (username != "guest") { + response.setStatusLine(metadata.httpVersion, 400, "bad request"); + body = "should never get here"; + } else { + // see RFC2617 for the description of this calculation + var A1 = "guest:secret:guest"; + var A2 = "GET:/auth/digest"; + var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":"); + var digest = H([H(A1), noncebits].join(":")); + + if (clientDigest == digest) { + response.setStatusLine(metadata.httpVersion, 200, "OK, authorized"); + body = "success"; + } else { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", authenticate, false); + body = "auth failed"; + } + } + } else { + // no header, send one + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", authenticate, false); + body = "failed, no header"; + } + + response.bodyOutputStream.write(body, body.length); +} + +function authShortDigest(metadata, response) { + // no header, send one + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", "Digest", false); +} + +let buildLargePayload = (function() { + let size = 33 * 1024; + let ret = ""; + return function() { + // Return cached value. + if (ret.length > 0) { + return ret; + } + for (let i = 0; i < size; i++) { + ret += "a"; + } + return ret; + }; +})(); + +function largeRealm(metadata, response) { + // test > 32KB realm tokens + var body; + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + 'Digest realm="' + buildLargePayload() + '", domain="foo"' + ); + + body = "need to authenticate"; + response.bodyOutputStream.write(body, body.length); +} + +function largeDomain(metadata, response) { + // test > 32KB domain tokens + var body; + + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader( + "WWW-Authenticate", + 'Digest realm="foo", domain="' + buildLargePayload() + '"' + ); + + body = "need to authenticate"; + response.bodyOutputStream.write(body, body.length); +} + +function test_large_realm() { + var chan = makeChan(URL + "/largeRealm", URL); + + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} + +function test_large_domain() { + var chan = makeChan(URL + "/largeDomain", URL); + + listener.expectedCode = 401; // Unauthorized + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_authpromptwrapper.js b/netwerk/test/unit/test_authpromptwrapper.js new file mode 100644 index 0000000000..91068dc692 --- /dev/null +++ b/netwerk/test/unit/test_authpromptwrapper.js @@ -0,0 +1,233 @@ +// NOTE: This tests code outside of Necko. The test still lives here because +// the contract is part of Necko. + +// TODO: +// - HTTPS +// - Proxies + +"use strict"; + +const nsIAuthInformation = Ci.nsIAuthInformation; +const nsIAuthPromptAdapterFactory = Ci.nsIAuthPromptAdapterFactory; + +function run_test() { + const contractID = "@mozilla.org/network/authprompt-adapter-factory;1"; + if (!(contractID in Cc)) { + print("No adapter factory found, skipping testing"); + return; + } + var adapter = Cc[contractID].getService(); + Assert.equal(adapter instanceof nsIAuthPromptAdapterFactory, true); + + // NOTE: xpconnect lets us get away with passing an empty object here + // For this part of the test, we only care that this function returns + // success + Assert.notEqual(adapter.createAdapter({}), null); + + const host = "www.mozilla.org"; + + var info = { + username: "", + password: "", + domain: "", + + flags: nsIAuthInformation.AUTH_HOST, + authenticationScheme: "basic", + realm: "secretrealm", + }; + + const CALLED_PROMPT = 1 << 0; + const CALLED_PROMPTUP = 1 << 1; + const CALLED_PROMPTP = 1 << 2; + function Prompt1() {} + Prompt1.prototype = { + called: 0, + rv: true, + + user: "foo\\bar", + pw: "bar", + + scheme: "http", + + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]), + + prompt: function ap1_prompt(title, text, realm, save, defaultText, result) { + this.called |= CALLED_PROMPT; + this.doChecks(text, realm); + return this.rv; + }, + + promptUsernameAndPassword: function ap1_promptUP( + title, + text, + realm, + savePW, + user, + pw + ) { + this.called |= CALLED_PROMPTUP; + this.doChecks(text, realm); + user.value = this.user; + pw.value = this.pw; + return this.rv; + }, + + promptPassword: function ap1_promptPW(title, text, realm, save, pwd) { + this.called |= CALLED_PROMPTP; + this.doChecks(text, realm); + pwd.value = this.pw; + return this.rv; + }, + + doChecks: function ap1_check(text, realm) { + Assert.equal(this.scheme + "://" + host + " (" + info.realm + ")", realm); + + Assert.notEqual(text.indexOf(host), -1); + if (info.flags & nsIAuthInformation.ONLY_PASSWORD) { + // Should have the username in the text + Assert.notEqual(text.indexOf(info.username), -1); + } else { + // Make sure that we show the realm if we have one and that we don't + // show "" otherwise + if (info.realm != "") { + Assert.notEqual(text.indexOf(info.realm), -1); + } else { + Assert.equal(text.indexOf('""'), -1); + } + // No explicit port in the URL; message should not contain -1 + // for those cases + Assert.equal(text.indexOf("-1"), -1); + } + }, + }; + + // Also have to make up a channel + var uri = NetUtil.newURI("http://" + host); + var chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + + function do_tests(expectedRV) { + var prompt1; + var wrapper; + + // 1: The simple case + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + wrapper = adapter.createAdapter(prompt1); + + var rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); + Assert.equal(info.password, prompt1.pw); + } + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 2: Only ask for a PW + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.ONLY_PASSWORD; + + // Initialize the username so that the prompt can show it + info.username = prompt1.user; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); // we initialized this + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.ONLY_PASSWORD; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 3: user, pw and domain + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.NEED_DOMAIN; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, "foo"); + Assert.equal(info.username, "bar"); + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.NEED_DOMAIN; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 4: username that doesn't contain a domain + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + info.flags |= nsIAuthInformation.NEED_DOMAIN; + + prompt1.user = "foo"; + + wrapper = adapter.createAdapter(prompt1); + rv = wrapper.promptAuth(chan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); + Assert.equal(info.password, prompt1.pw); + } + + info.flags &= ~nsIAuthInformation.NEED_DOMAIN; + + info.domain = ""; + info.username = ""; + info.password = ""; + + // 5: FTP + var uri2 = NetUtil.newURI("ftp://" + host); + var ftpchan = NetUtil.newChannel({ + uri: uri2, + loadUsingSystemPrincipal: true, + }); + + prompt1 = new Prompt1(); + prompt1.rv = expectedRV; + prompt1.scheme = "ftp"; + + wrapper = adapter.createAdapter(prompt1); + var rv = wrapper.promptAuth(ftpchan, 0, info); + Assert.equal(rv, prompt1.rv); + Assert.equal(prompt1.called, CALLED_PROMPTUP); + + if (rv) { + Assert.equal(info.domain, ""); + Assert.equal(info.username, prompt1.user); + Assert.equal(info.password, prompt1.pw); + } + + info.domain = ""; + info.username = ""; + info.password = ""; + } + do_tests(true); + do_tests(false); +} diff --git a/netwerk/test/unit/test_backgroundfilesaver.js b/netwerk/test/unit/test_backgroundfilesaver.js new file mode 100644 index 0000000000..696884f356 --- /dev/null +++ b/netwerk/test/unit/test_backgroundfilesaver.js @@ -0,0 +1,775 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests components that implement nsIBackgroundFileSaver. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Globals + +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "NetUtil", + "resource://gre/modules/NetUtil.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "FileTestUtils", + "resource://testing-common/FileTestUtils.jsm" +); + +const BackgroundFileSaverOutputStream = Components.Constructor( + "@mozilla.org/network/background-file-saver;1?mode=outputstream", + "nsIBackgroundFileSaver" +); + +const BackgroundFileSaverStreamListener = Components.Constructor( + "@mozilla.org/network/background-file-saver;1?mode=streamlistener", + "nsIBackgroundFileSaver" +); + +const StringInputStream = Components.Constructor( + "@mozilla.org/io/string-input-stream;1", + "nsIStringInputStream", + "setData" +); + +const REQUEST_SUSPEND_AT = 1024 * 1024 * 4; +const TEST_DATA_SHORT = "This test string is written to the file."; +const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt"; +const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt"; +const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt"; + +// A map of test data length to the expected SHA-256 hashes +const EXPECTED_HASHES = { + // No data + 0: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + // TEST_DATA_SHORT + 40: "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192", + // TEST_DATA_SHORT + TEST_DATA_SHORT + 80: "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58", + // TEST_DATA_LONG + 4718592: "372cb9e5ce7b76d3e2a5042e78aa72dcf973e659a262c61b7ff51df74b36767b", + // TEST_DATA_LONG + TEST_DATA_LONG + 9437184: "693e4f8c6855a6fed4f5f9370d12cc53105672f3ff69783581e7d925984c41d3", +}; + +const gTextDecoder = new TextDecoder(); + +// Generate a long string of data in a moderately fast way. +const TEST_256_CHARS = new Array(257).join("-"); +const DESIRED_LENGTH = REQUEST_SUSPEND_AT * 1.125; +const TEST_DATA_LONG = new Array(1 + DESIRED_LENGTH / 256).join(TEST_256_CHARS); +Assert.equal(TEST_DATA_LONG.length, DESIRED_LENGTH); + +/** + * Returns a reference to a temporary file that is guaranteed not to exist and + * is cleaned up later. See FileTestUtils.getTempFile for details. + */ +function getTempFile(leafName) { + return FileTestUtils.getTempFile(leafName); +} + +/** + * Helper function for converting a binary blob to its hex equivalent. + * + * @param str + * String possibly containing non-printable chars. + * @return A hex-encoded string. + */ +function toHex(str) { + var hex = ""; + for (var i = 0; i < str.length; i++) { + hex += ("0" + str.charCodeAt(i).toString(16)).slice(-2); + } + return hex; +} + +/** + * Ensures that the given file contents are equal to the given string. + * + * @param aFile + * nsIFile whose contents should be verified. + * @param aExpectedContents + * String containing the octets that are expected in the file. + * + * @return {Promise} + * @resolves When the operation completes. + * @rejects Never. + */ +function promiseVerifyContents(aFile, aExpectedContents) { + return new Promise(resolve => { + NetUtil.asyncFetch( + { + uri: NetUtil.newURI(aFile), + loadUsingSystemPrincipal: true, + }, + function(aInputStream, aStatus) { + Assert.ok(Components.isSuccessCode(aStatus)); + let contents = NetUtil.readInputStreamToString( + aInputStream, + aInputStream.available() + ); + if (contents.length <= TEST_DATA_SHORT.length * 2) { + Assert.equal(contents, aExpectedContents); + } else { + // Do not print the entire content string to the test log. + Assert.equal(contents.length, aExpectedContents.length); + Assert.ok(contents == aExpectedContents); + } + resolve(); + } + ); + }); +} + +/** + * Waits for the given saver object to complete. + * + * @param aSaver + * The saver, with the output stream or a stream listener implementation. + * @param aOnTargetChangeFn + * Optional callback invoked with the target file name when it changes. + * + * @return {Promise} + * @resolves When onSaveComplete is called with a success code. + * @rejects With an exception, if onSaveComplete is called with a failure code. + */ +function promiseSaverComplete(aSaver, aOnTargetChangeFn) { + return new Promise((resolve, reject) => { + aSaver.observer = { + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) { + if (aOnTargetChangeFn) { + aOnTargetChangeFn(aTarget); + } + }, + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) { + if (Components.isSuccessCode(aStatus)) { + resolve(); + } else { + reject(new Components.Exception("Saver failed.", aStatus)); + } + }, + }; + }); +} + +/** + * Feeds a string to a BackgroundFileSaverOutputStream. + * + * @param aSourceString + * The source data to copy. + * @param aSaverOutputStream + * The BackgroundFileSaverOutputStream to feed. + * @param aCloseWhenDone + * If true, the output stream will be closed when the copy finishes. + * + * @return {Promise} + * @resolves When the copy completes with a success code. + * @rejects With an exception, if the copy fails. + */ +function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) { + return new Promise((resolve, reject) => { + let inputStream = new StringInputStream( + aSourceString, + aSourceString.length + ); + let copier = Cc[ + "@mozilla.org/network/async-stream-copier;1" + ].createInstance(Ci.nsIAsyncStreamCopier); + copier.init( + inputStream, + aSaverOutputStream, + null, + false, + true, + 0x8000, + true, + aCloseWhenDone + ); + copier.asyncCopy( + { + onStartRequest() {}, + onStopRequest(aRequest, aStatusCode) { + if (Components.isSuccessCode(aStatusCode)) { + resolve(); + } else { + reject(new Components.Exception(aStatusCode)); + } + }, + }, + null + ); + }); +} + +/** + * Feeds a string to a BackgroundFileSaverStreamListener. + * + * @param aSourceString + * The source data to copy. + * @param aSaverStreamListener + * The BackgroundFileSaverStreamListener to feed. + * @param aCloseWhenDone + * If true, the output stream will be closed when the copy finishes. + * + * @return {Promise} + * @resolves When the operation completes with a success code. + * @rejects With an exception, if the operation fails. + */ +function promisePumpToSaver( + aSourceString, + aSaverStreamListener, + aCloseWhenDone +) { + return new Promise((resolve, reject) => { + aSaverStreamListener.QueryInterface(Ci.nsIStreamListener); + let inputStream = new StringInputStream( + aSourceString, + aSourceString.length + ); + let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(inputStream, 0, 0, true); + pump.asyncRead({ + onStartRequest: function PPTS_onStartRequest(aRequest) { + aSaverStreamListener.onStartRequest(aRequest); + }, + onStopRequest: function PPTS_onStopRequest(aRequest, aStatusCode) { + aSaverStreamListener.onStopRequest(aRequest, aStatusCode); + if (Components.isSuccessCode(aStatusCode)) { + resolve(); + } else { + reject(new Components.Exception(aStatusCode)); + } + }, + onDataAvailable: function PPTS_onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ) { + aSaverStreamListener.onDataAvailable( + aRequest, + aInputStream, + aOffset, + aCount + ); + }, + }); + }); +} + +var gStillRunning = true; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +add_task(function test_setup() { + // Wait 10 minutes, that is half of the external xpcshell timeout. + do_timeout(10 * 60 * 1000, function() { + if (gStillRunning) { + do_throw("Test timed out."); + } + }); +}); + +add_task(async function test_normal() { + // This test demonstrates the most basic use case. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Create the object implementing the output stream. + let saver = new BackgroundFileSaverOutputStream(); + + // Set up callbacks for completion and target file name change. + let receivedOnTargetChange = false; + function onTargetChange(aTarget) { + Assert.ok(destFile.equals(aTarget)); + receivedOnTargetChange = true; + } + let completionPromise = promiseSaverComplete(saver, onTargetChange); + + // Set the target file. + saver.setTarget(destFile, false); + + // Write some data and close the output stream. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // Indicate that we are ready to finish, and wait for a successful callback. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Only after we receive the completion notification, we can also be sure that + // we've received the target file name change notification before it. + Assert.ok(receivedOnTargetChange); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_combinations() { + let initialFile = getTempFile(TEST_FILE_NAME_1); + let renamedFile = getTempFile(TEST_FILE_NAME_2); + + // Keep track of the current file. + let currentFile = null; + function onTargetChange(aTarget) { + currentFile = null; + info("Target file changed to: " + aTarget.leafName); + currentFile = aTarget; + } + + // Tests various combinations of events and behaviors for both the stream + // listener and the output stream implementations. + for (let testFlags = 0; testFlags < 32; testFlags++) { + let keepPartialOnFailure = !!(testFlags & 1); + let renameAtSomePoint = !!(testFlags & 2); + let cancelAtSomePoint = !!(testFlags & 4); + let useStreamListener = !!(testFlags & 8); + let useLongData = !!(testFlags & 16); + + let startTime = Date.now(); + info( + "Starting keepPartialOnFailure = " + + keepPartialOnFailure + + ", renameAtSomePoint = " + + renameAtSomePoint + + ", cancelAtSomePoint = " + + cancelAtSomePoint + + ", useStreamListener = " + + useStreamListener + + ", useLongData = " + + useLongData + ); + + // Create the object and register the observers. + currentFile = null; + let saver = useStreamListener + ? new BackgroundFileSaverStreamListener() + : new BackgroundFileSaverOutputStream(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver, onTargetChange); + + // Start feeding the first chunk of data to the saver. In case we are using + // the stream listener, we only write one chunk. + let testData = useLongData ? TEST_DATA_LONG : TEST_DATA_SHORT; + let feedPromise = useStreamListener + ? promisePumpToSaver(testData + testData, saver) + : promiseCopyToSaver(testData, saver, false); + + // Set a target output file. + saver.setTarget(initialFile, keepPartialOnFailure); + + // Wait for the first chunk of data to be copied. + await feedPromise; + + if (renameAtSomePoint) { + saver.setTarget(renamedFile, keepPartialOnFailure); + } + + if (cancelAtSomePoint) { + saver.finish(Cr.NS_ERROR_FAILURE); + } + + // Feed the second chunk of data to the saver. + if (!useStreamListener) { + await promiseCopyToSaver(testData, saver, true); + } + + // Wait for completion, and ensure we succeeded or failed as expected. + if (!cancelAtSomePoint) { + saver.finish(Cr.NS_OK); + } + try { + await completionPromise; + if (cancelAtSomePoint) { + do_throw("Failure expected."); + } + } catch (ex) { + if (!cancelAtSomePoint || ex.result != Cr.NS_ERROR_FAILURE) { + throw ex; + } + } + + if (!cancelAtSomePoint) { + // In this case, the file must exist. + Assert.ok(currentFile.exists()); + let expectedContents = testData + testData; + await promiseVerifyContents(currentFile, expectedContents); + Assert.equal( + EXPECTED_HASHES[expectedContents.length], + toHex(saver.sha256Hash) + ); + currentFile.remove(false); + + // If the target was really renamed, the old file should not exist. + if (renamedFile.equals(currentFile)) { + Assert.ok(!initialFile.exists()); + } + } else if (!keepPartialOnFailure) { + // In this case, the file must not exist. + Assert.ok(!initialFile.exists()); + Assert.ok(!renamedFile.exists()); + } else { + // In this case, the file may or may not exist, because canceling can + // interrupt the asynchronous operation at any point, even before the file + // has been created for the first time. + if (initialFile.exists()) { + initialFile.remove(false); + } + if (renamedFile.exists()) { + renamedFile.remove(false); + } + } + + info("Test case completed in " + (Date.now() - startTime) + " ms."); + } +}); + +add_task(async function test_setTarget_after_close_stream() { + // This test checks the case where we close the output stream before we call + // the setTarget method. All the data should be buffered and written anyway. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + // Copy some data to the output stream of the file saver. This data must + // be shorter than the internal component's pipe buffer for the test to + // succeed, because otherwise the test would block waiting for the write to + // complete. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // Set the target file and wait for the output to finish. + saver.setTarget(destFile, false); + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + Assert.equal( + EXPECTED_HASHES[TEST_DATA_SHORT.length], + toHex(saver.sha256Hash) + ); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_setTarget_fast() { + // This test checks a fast rename of the target file. + let destFile1 = getTempFile(TEST_FILE_NAME_1); + let destFile2 = getTempFile(TEST_FILE_NAME_2); + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + // Set the initial name after the stream is closed, then rename immediately. + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + saver.setTarget(destFile1, false); + saver.setTarget(destFile2, false); + + // Wait for all the operations to complete. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results and clean up. + Assert.ok(!destFile1.exists()); + await promiseVerifyContents(destFile2, TEST_DATA_SHORT); + destFile2.remove(false); +}); + +add_task(async function test_setTarget_multiple() { + // This test checks multiple renames of the target file. + let destFile = getTempFile(TEST_FILE_NAME_1); + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + // Rename both before and after the stream is closed. + saver.setTarget(getTempFile(TEST_FILE_NAME_2), false); + saver.setTarget(getTempFile(TEST_FILE_NAME_3), false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + saver.setTarget(getTempFile(TEST_FILE_NAME_2), false); + saver.setTarget(destFile, false); + + // Wait for all the operations to complete. + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results and clean up. + Assert.ok(!getTempFile(TEST_FILE_NAME_2).exists()); + Assert.ok(!getTempFile(TEST_FILE_NAME_3).exists()); + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + destFile.remove(false); +}); + +add_task(async function test_enableAppend() { + // This test checks append mode with hashing disabled. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + let expectedContents = + i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG; + await promiseVerifyContents(destFile, expectedContents); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_enableAppend_setTarget_fast() { + // This test checks a fast rename of the target file in append mode. + let destFile1 = getTempFile(TEST_FILE_NAME_1); + let destFile2 = getTempFile(TEST_FILE_NAME_2); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + let completionPromise = promiseSaverComplete(saver); + + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + // The first time, we start appending to the first file and rename to the + // second file. The second time, we start appending to the second file, + // that was created the first time, and rename back to the first file. + let firstFile = i == 0 ? destFile1 : destFile2; + let secondFile = i == 0 ? destFile2 : destFile1; + saver.setTarget(firstFile, false); + saver.setTarget(secondFile, false); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.ok(!firstFile.exists()); + let expectedContents = + i == 0 ? TEST_DATA_SHORT : TEST_DATA_SHORT + TEST_DATA_SHORT; + await promiseVerifyContents(secondFile, expectedContents); + } + + // Clean up. + destFile1.remove(false); +}); + +add_task(async function test_enableAppend_hash() { + // This test checks append mode, also verifying that the computed hash + // includes the contents of the existing data. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test the case where the file does not already exists first, then the case + // where the file already exists. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + saver.enableAppend(); + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_LONG, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + let expectedContents = + i == 0 ? TEST_DATA_LONG : TEST_DATA_LONG + TEST_DATA_LONG; + await promiseVerifyContents(destFile, expectedContents); + Assert.equal( + EXPECTED_HASHES[expectedContents.length], + toHex(saver.sha256Hash) + ); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_finish_only() { + // This test checks creating the object and doing nothing. + let saver = new BackgroundFileSaverOutputStream(); + function onTargetChange(aTarget) { + do_throw("Should not receive the onTargetChange notification."); + } + let completionPromise = promiseSaverComplete(saver, onTargetChange); + saver.finish(Cr.NS_OK); + await completionPromise; +}); + +add_task(async function test_empty() { + // This test checks we still create an empty file when no data is fed. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.ok(destFile.exists()); + Assert.equal(destFile.fileSize, 0); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_empty_hash() { + // This test checks the hash of an empty file, both in normal and append mode. + let destFile = getTempFile(TEST_FILE_NAME_1); + + // Test normal mode first, then append mode. + for (let i = 0; i < 2; i++) { + let saver = new BackgroundFileSaverOutputStream(); + if (i == 1) { + saver.enableAppend(); + } + saver.enableSha256(); + let completionPromise = promiseSaverComplete(saver); + + saver.setTarget(destFile, false); + await promiseCopyToSaver("", saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + + // Verify results. + Assert.equal(destFile.fileSize, 0); + Assert.equal(EXPECTED_HASHES[0], toHex(saver.sha256Hash)); + } + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_invalid_hash() { + let saver = new BackgroundFileSaverStreamListener(); + let completionPromise = promiseSaverComplete(saver); + // We shouldn't be able to get the hash if hashing hasn't been enabled + try { + saver.sha256Hash; + do_throw("Shouldn't be able to get hash if hashing not enabled"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + // Enable hashing, but don't feed any data to saver + saver.enableSha256(); + let destFile = getTempFile(TEST_FILE_NAME_1); + saver.setTarget(destFile, false); + // We don't wait on promiseSaverComplete, so the hash getter can run before + // or after onSaveComplete is called. However, the expected behavior is the + // same in both cases since the hash is only valid when the save completes + // successfully. + saver.finish(Cr.NS_ERROR_FAILURE); + try { + saver.sha256Hash; + do_throw("Shouldn't be able to get hash if save did not succeed"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + // Wait for completion so that the worker thread finishes dealing with the + // target file. We expect it to fail. + try { + await completionPromise; + do_throw("completionPromise should throw"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FAILURE) { + throw ex; + } + } +}); + +add_task(async function test_signature() { + // Check that we get a signature if the saver is finished. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + + try { + saver.signatureInfo; + do_throw("Can't get signature if saver is not complete"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + + saver.enableSignatureInfo(); + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + await promiseVerifyContents(destFile, TEST_DATA_SHORT); + + // signatureInfo is an empty nsIArray + Assert.equal(0, saver.signatureInfo.length); + + // Clean up. + destFile.remove(false); +}); + +add_task(async function test_signature_not_enabled() { + // Check that we get a signature if the saver is finished on Windows. + let destFile = getTempFile(TEST_FILE_NAME_1); + + let saver = new BackgroundFileSaverOutputStream(); + let completionPromise = promiseSaverComplete(saver); + saver.setTarget(destFile, false); + await promiseCopyToSaver(TEST_DATA_SHORT, saver, true); + + saver.finish(Cr.NS_OK); + await completionPromise; + try { + saver.signatureInfo; + do_throw("Can't get signature if not enabled"); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_NOT_AVAILABLE) { + throw ex; + } + } + + // Clean up. + destFile.remove(false); +}); + +add_task(function test_teardown() { + gStillRunning = false; +}); diff --git a/netwerk/test/unit/test_be_conservative.js b/netwerk/test/unit/test_be_conservative.js new file mode 100644 index 0000000000..f5ef6b7bb7 --- /dev/null +++ b/netwerk/test/unit/test_be_conservative.js @@ -0,0 +1,247 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of +// advanced TLS features that may cause compatibility issues. Does so by +// starting a TLS server that requires the advanced features and then ensuring +// that a client that is set to be conservative will fail when connecting. + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +function getCert() { + return new Promise((resolve, reject) => { + let certService = Cc[ + "@mozilla.org/security/local-cert-service;1" + ].getService(Ci.nsILocalCertService); + certService.getOrCreateCert("beConservative-test", { + handleCert(c, rv) { + if (rv) { + reject(rv); + return; + } + resolve(c); + }, + }); + }); +} + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityInfo.QueryInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver(input, output); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + } + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + } +} + +function startServer(cert, minServerVersion, maxServerVersion) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + tlsServer.asyncListen(new ServerSocketListener()); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + let overrideBits = + Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH; + certOverrideService.rememberValidityOverride( + hostname, + port, + cert, + overrideBits, + true + ); +} + +function startClient(port, beConservative, expectSuccess) { + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.beConservative = beConservative; + return new Promise((resolve, reject) => { + req.onload = () => { + ok( + expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event` + ); + equal(req.responseText, "OK", "response text should be 'OK'"); + resolve(); + }; + req.onerror = () => { + ok( + !expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error` + ); + resolve(); + }; + + req.send(); + }); +} + +add_task(async function() { + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + let cert = await getCert(); + + // First run a server that accepts TLS 1.2 and 1.3. A conservative client + // should succeed in connecting. + let server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + true /*should succeed*/ + ); + server.close(); + + // Now run a server that only accepts TLS 1.3. A conservative client will not + // succeed in this case. + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + false /*should fail*/ + ); + + // However, a non-conservative client should succeed. + await startClient( + server.port, + false /*don't be conservative*/, + true /*should succeed*/ + ); + server.close(); +}); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); diff --git a/netwerk/test/unit/test_be_conservative_error_handling.js b/netwerk/test/unit/test_be_conservative_error_handling.js new file mode 100644 index 0000000000..190fbba157 --- /dev/null +++ b/netwerk/test/unit/test_be_conservative_error_handling.js @@ -0,0 +1,242 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; + +// Tests that nsIHttpChannelInternal.beConservative correctly limits the use of +// advanced TLS features that may cause compatibility issues. Does so by +// starting a TLS server that requires the advanced features and then ensuring +// that a client that is set to be conservative will fail when connecting. + +// Get a profile directory and ensure PSM initializes NSS. +do_get_profile(); +Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + +function getCert() { + return new Promise((resolve, reject) => { + let certService = Cc[ + "@mozilla.org/security/local-cert-service;1" + ].getService(Ci.nsILocalCertService); + certService.getOrCreateCert("beConservative-test", { + handleCert(c, rv) { + if (rv) { + reject(rv); + return; + } + resolve(c); + }, + }); + }); +} + +class InputStreamCallback { + constructor(output) { + this.output = output; + this.stopped = false; + } + + onInputStreamReady(stream) { + info("input stream ready"); + if (this.stopped) { + info("input stream callback stopped - bailing"); + return; + } + let available = 0; + try { + available = stream.available(); + } catch (e) { + // onInputStreamReady may fire when the stream has been closed. + equal( + e.result, + Cr.NS_BASE_STREAM_CLOSED, + "error should be NS_BASE_STREAM_CLOSED" + ); + } + if (available > 0) { + let request = NetUtil.readInputStreamToString(stream, available, { + charset: "utf8", + }); + ok( + request.startsWith("GET / HTTP/1.1\r\n"), + "Should get a simple GET / HTTP/1.1 request" + ); + let response = + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 2\r\n" + + "Content-Type: text/plain\r\n" + + "\r\nOK"; + let written = this.output.write(response, response.length); + equal( + written, + response.length, + "should have been able to write entire response" + ); + } + this.output.close(); + info("done with input stream ready"); + } + + stop() { + this.stopped = true; + this.output.close(); + } +} + +class TLSServerSecurityObserver { + constructor(input, output) { + this.input = input; + this.output = output; + this.callbacks = []; + this.stopped = false; + } + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + info(`TLS version used: ${status.tlsVersionUsed}`); + + if (this.stopped) { + info("handshake done callback stopped - bailing"); + return; + } + + let callback = new InputStreamCallback(this.output); + this.callbacks.push(callback); + this.input.asyncWait(callback, 0, 0, Services.tm.currentThread); + } + + stop() { + this.stopped = true; + this.input.close(); + this.output.close(); + this.callbacks.forEach(callback => { + callback.stop(); + }); + } +} + +class ServerSocketListener { + constructor() { + this.securityObservers = []; + } + + onSocketAccepted(socket, transport) { + info("accepted TLS client connection"); + let connectionInfo = transport.securityInfo.QueryInterface( + Ci.nsITLSServerConnectionInfo + ); + let input = transport.openInputStream(0, 0, 0); + let output = transport.openOutputStream(0, 0, 0); + let securityObserver = new TLSServerSecurityObserver(input, output); + this.securityObservers.push(securityObserver); + connectionInfo.setSecurityObserver(securityObserver); + } + + // For some reason we get input stream callback events after we've stopped + // listening, so this ensures we just drop those events. + onStopListening() { + info("onStopListening"); + this.securityObservers.forEach(observer => { + observer.stop(); + }); + } +} + +function startServer(cert, minServerVersion, maxServerVersion) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + tlsServer.setVersionRange(minServerVersion, maxServerVersion); + tlsServer.setSessionTickets(false); + tlsServer.asyncListen(new ServerSocketListener()); + return tlsServer; +} + +const hostname = "example.com"; + +function storeCertOverride(port, cert) { + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + let overrideBits = + Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH; + certOverrideService.rememberValidityOverride( + hostname, + port, + cert, + overrideBits, + true + ); +} + +function startClient(port, beConservative, expectSuccess) { + let req = new XMLHttpRequest(); + req.open("GET", `https://${hostname}:${port}`); + let internalChannel = req.channel.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.beConservative = beConservative; + return new Promise((resolve, reject) => { + req.onload = () => { + ok( + expectSuccess, + `should ${expectSuccess ? "" : "not "}have gotten load event` + ); + equal(req.responseText, "OK", "response text should be 'OK'"); + resolve(); + }; + req.onerror = () => { + ok( + !expectSuccess, + `should ${!expectSuccess ? "" : "not "}have gotten an error` + ); + resolve(); + }; + + req.send(); + }); +} + +add_task(async function() { + // Restrict to only TLS 1.3. + Services.prefs.setIntPref("security.tls.version.min", 4); + Services.prefs.setIntPref("security.tls.version.max", 4); + Services.prefs.setCharPref("network.dns.localDomains", hostname); + let cert = await getCert(); + + // Run a server that accepts TLS 1.2 and 1.3. The connection should succeed. + let server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_2, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + true /*should succeed*/ + ); + server.close(); + + // Now run a server that only accepts TLS 1.3. A conservative client will not + // succeed in this case. + server = startServer( + cert, + Ci.nsITLSClientStatus.TLS_VERSION_1_3, + Ci.nsITLSClientStatus.TLS_VERSION_1_3 + ); + storeCertOverride(server.port, cert); + await startClient( + server.port, + true /*be conservative*/, + false /*should fail*/ + ); + server.close(); +}); + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("security.tls.version.min"); + Services.prefs.clearUserPref("security.tls.version.max"); + Services.prefs.clearUserPref("network.dns.localDomains"); +}); diff --git a/netwerk/test/unit/test_blob_channelname.js b/netwerk/test/unit/test_blob_channelname.js new file mode 100644 index 0000000000..c1a09272da --- /dev/null +++ b/netwerk/test/unit/test_blob_channelname.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function channelname() { + var file = new File( + [new Blob(["test"], { type: "text/plain" })], + "test-name" + ); + var url = URL.createObjectURL(file); + var channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + + let inputStream = channel.open(); + ok(inputStream, "Should be able to open channel"); + ok( + inputStream.QueryInterface(Ci.nsIAsyncInputStream), + "Stream should support async operations" + ); + + await new Promise(resolve => { + inputStream.asyncWait( + () => { + let available = inputStream.available(); + ok(available, "There should be data to read"); + Assert.equal( + channel.contentDispositionFilename, + "test-name", + "filename matches" + ); + resolve(); + }, + 0, + 0, + Services.tm.mainThread + ); + }); + + inputStream.close(); + channel.cancel(Cr.NS_ERROR_FAILURE); +}); diff --git a/netwerk/test/unit/test_brotli_http.js b/netwerk/test/unit/test_brotli_http.js new file mode 100644 index 0000000000..5c3d2452f0 --- /dev/null +++ b/netwerk/test/unit/test_brotli_http.js @@ -0,0 +1,44 @@ +// This test exists mostly as documentation that +// Firefox can load brotli files over HTTP if we set the proper pref. + +"use strict"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "br", false); + response.write("\x0b\x02\x80hello\x03"); +} + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +add_task(async function test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + Services.prefs.setCharPref( + "network.http.accept-encoding", + "gzip, deflate, br" + ); + + let chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true }); + let [, buff] = await new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + (req, buff) => { + resolve([req, buff]); + }, + null, + CL_IGNORE_CL + ) + ); + }); + equal(buff, "hello"); + Services.prefs.clearUserPref("network.http.accept-encoding"); + await httpServer.stop(); +}); diff --git a/netwerk/test/unit/test_bug1064258.js b/netwerk/test/unit/test_bug1064258.js new file mode 100644 index 0000000000..7e9ce74409 --- /dev/null +++ b/netwerk/test/unit/test_bug1064258.js @@ -0,0 +1,142 @@ +/** + * Check how nsICachingChannel.cacheOnlyMetadata works. + * - all channels involved in this test are set cacheOnlyMetadata = true + * - do a previously uncached request for a long living content + * - check we have downloaded the content from the server (channel provides it) + * - check the entry has metadata, but zero-length content + * - load the same URL again, now cached + * - check the channel is giving no content (no call to OnDataAvailable) but succeeds + * - repeat again, but for a different URL that is not cached (immediately expires) + * - only difference is that we get a newer version of the content from the server during the second request + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody1 = "response body 1"; +const responseBody2a = "response body 2a"; +const responseBody2b = "response body 2b"; + +function contentHandler1(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-control", "max-age=999999"); + response.bodyOutputStream.write(responseBody1, responseBody1.length); +} + +var content2passCount = 0; + +function contentHandler2(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-control", "no-cache"); + switch (content2passCount++) { + case 0: + response.setHeader("ETag", "testetag"); + response.bodyOutputStream.write(responseBody2a, responseBody2a.length); + break; + case 1: + Assert.ok(metadata.hasHeader("If-None-Match")); + Assert.equal(metadata.getHeader("If-None-Match"), "testetag"); + response.bodyOutputStream.write(responseBody2b, responseBody2b.length); + break; + default: + throw "Unexpected request in the test"; + } +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content1", contentHandler1); + httpServer.registerPathHandler("/content2", contentHandler2); + httpServer.start(-1); + + run_test_content1a(); + do_test_pending(); +} + +function run_test_content1a() { + var chan = make_channel(URL + "/content1"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener1a, null)); +} + +function contentListener1a(request, buffer) { + Assert.equal(buffer, responseBody1); + + asyncOpenCacheEntry(URL + "/content1", "disk", 0, null, cacheCheck1); +} + +function cacheCheck1(status, entry) { + Assert.equal(status, 0); + Assert.equal(entry.dataSize, 0); + try { + Assert.notEqual(entry.getMetaDataElement("response-head"), null); + } catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content1"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener1b, null, CL_IGNORE_CL)); +} + +function contentListener1b(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.requestMethod, "GET"); + Assert.equal(request.responseStatus, 200); + Assert.equal(request.getResponseHeader("Cache-control"), "max-age=999999"); + + Assert.equal(buffer, ""); + run_test_content2a(); +} + +// Now same set of steps but this time for an immediately expiring content. + +function run_test_content2a() { + var chan = make_channel(URL + "/content2"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener2a, null)); +} + +function contentListener2a(request, buffer) { + Assert.equal(buffer, responseBody2a); + + asyncOpenCacheEntry(URL + "/content2", "disk", 0, null, cacheCheck2); +} + +function cacheCheck2(status, entry) { + Assert.equal(status, 0); + Assert.equal(entry.dataSize, 0); + try { + Assert.notEqual(entry.getMetaDataElement("response-head"), null); + Assert.ok( + entry.getMetaDataElement("response-head").match("etag: testetag") + ); + } catch (ex) { + do_throw("Missing response head"); + } + + var chan = make_channel(URL + "/content2"); + let caching = chan.QueryInterface(Ci.nsICachingChannel); + caching.cacheOnlyMetadata = true; + chan.asyncOpen(new ChannelListener(contentListener2b, null)); +} + +function contentListener2b(request, buffer) { + Assert.equal(buffer, responseBody2b); + + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_bug1177909.js b/netwerk/test/unit/test_bug1177909.js new file mode 100644 index 0000000000..a4e6117cbc --- /dev/null +++ b/netwerk/test/unit/test_bug1177909.js @@ -0,0 +1,194 @@ +"use strict"; + +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gProxyService", + "@mozilla.org/network/protocol-proxy-service;1", + "nsIProtocolProxyService" +); + +XPCOMUtils.defineLazyGetter(this, "systemSettings", function() { + return { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + mainThreadOnly: true, + PACURI: null, + + getProxyForURI(aSpec, aScheme, aHost, aPort) { + if (aPort != -1) { + return "SOCKS5 http://localhost:9050"; + } + if (aScheme == "http" || aScheme == "ftp") { + return "PROXY http://localhost:8080"; + } + if (aScheme == "https") { + return "HTTPS https://localhost:8080"; + } + return "DIRECT"; + }, + }; +}); + +let gMockProxy = MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemSettings +); + +registerCleanupFunction(() => { + MockRegistrar.unregister(gMockProxy); +}); + +function makeChannel(uri) { + return NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); +} + +async function TestProxyType(chan, flags) { + const prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + return new Promise((resolve, reject) => { + gProxyService.asyncResolve(chan, flags, { + onProxyAvailable(req, uri, pi, status) { + resolve(pi); + }, + }); + }); +} + +async function TestProxyTypeByURI(uri) { + return TestProxyType(makeChannel(uri), 0); +} + +add_task(async function testHttpProxy() { + let pi = await TestProxyTypeByURI("http://www.mozilla.org/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "http", "Expected proxy type to be http"); +}); + +add_task(async function testHttpsProxy() { + let pi = await TestProxyTypeByURI("https://www.mozilla.org/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); + +if (Services.prefs.getBoolPref("network.ftp.enabled")) { + add_task(async function testFtpProxy() { + let pi = await TestProxyTypeByURI("ftp://ftp.mozilla.org/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "http", "Expected proxy type to be http"); + }); +} + +add_task(async function testSocksProxy() { + let pi = await TestProxyTypeByURI("http://www.mozilla.org:1234/"); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 9050, "Expected proxy port to be 8080"); + equal(pi.type, "socks", "Expected proxy type to be http"); +}); + +add_task(async function testDirectProxy() { + // Do what |WebSocketChannel::AsyncOpen| do, but do not prefer https proxy. + let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("wss://ws.mozilla.org/") + .finalize(); + let uri = proxyURI + .mutate() + .setScheme("https") + .finalize(); + + let ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + let chan = ioService.newChannelFromURIWithProxyFlags( + uri, + proxyURI, + 0, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, 0); + equal(pi, null, "Expected proxy host to be null"); +}); + +add_task(async function testWebSocketProxy() { + // Do what |WebSocketChannel::AsyncOpen| do + let proxyURI = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("wss://ws.mozilla.org/") + .finalize(); + let uri = proxyURI + .mutate() + .setScheme("https") + .finalize(); + + let proxyFlags = + Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY | + Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + + let ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + let chan = ioService.newChannelFromURIWithProxyFlags( + uri, + proxyURI, + proxyFlags, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); + +add_task(async function testPreferHttpsProxy() { + let uri = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec("http://mozilla.org/") + .finalize(); + let proxyFlags = Ci.nsIProtocolProxyService.RESOLVE_PREFER_HTTPS_PROXY; + + let ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + let chan = ioService.newChannelFromURIWithProxyFlags( + uri, + null, + proxyFlags, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + + let pi = await TestProxyType(chan, proxyFlags); + equal(pi.host, "localhost", "Expected proxy host to be localhost"); + equal(pi.port, 8080, "Expected proxy port to be 8080"); + equal(pi.type, "https", "Expected proxy type to be https"); +}); diff --git a/netwerk/test/unit/test_bug1195415.js b/netwerk/test/unit/test_bug1195415.js new file mode 100644 index 0000000000..491fd2d753 --- /dev/null +++ b/netwerk/test/unit/test_bug1195415.js @@ -0,0 +1,163 @@ +// Test for bug 1195415 + +"use strict"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + + // NON-UNICODE + var uri = ios.newURI("http://foo.com/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com"); + uri = uri + .mutate() + .setPort(90) + .finalize(); + var prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:90"); + Assert.equal(prin.origin, "http://foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:10"); + uri = uri + .mutate() + .setPort(500) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:500"); + Assert.equal(prin.origin, "http://foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri + .mutate() + .setPort(20) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com:20"); + Assert.equal(prin.origin, "http://foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri + .mutate() + .setPort(-1) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com"); + Assert.equal(prin.origin, "http://foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "foo.com:5000"); + uri = uri + .mutate() + .setPort(80) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "foo.com"); + Assert.equal(prin.origin, "http://foo.com"); + + // UNICODE + uri = ios.newURI("http://jos\u00e9.example.net.ch/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + uri = uri + .mutate() + .setPort(90) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:90"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:90"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:10/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:10"); + uri = uri + .mutate() + .setPort(500) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:500"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:500"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri + .mutate() + .setPort(20) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:20"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch:20"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri + .mutate() + .setPort(-1) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch"); + + uri = ios.newURI("http://jos\u00e9.example.net.ch:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch:5000"); + uri = uri + .mutate() + .setPort(80) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "xn--jos-dma.example.net.ch"); + Assert.equal(prin.origin, "http://xn--jos-dma.example.net.ch"); + + // ipv6 + uri = ios.newURI("http://[123:45::678]/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + uri = uri + .mutate() + .setPort(90) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:90"); + Assert.equal(prin.origin, "http://[123:45::678]:90"); + + uri = ios.newURI("http://[123:45::678]:10/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:10"); + uri = uri + .mutate() + .setPort(500) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:500"); + Assert.equal(prin.origin, "http://[123:45::678]:500"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri + .mutate() + .setPort(20) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]:20"); + Assert.equal(prin.origin, "http://[123:45::678]:20"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri + .mutate() + .setPort(-1) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + Assert.equal(prin.origin, "http://[123:45::678]"); + + uri = ios.newURI("http://[123:45::678]:5000/file.txt"); + Assert.equal(uri.asciiHostPort, "[123:45::678]:5000"); + uri = uri + .mutate() + .setPort(80) + .finalize(); + prin = ssm.createContentPrincipal(uri, {}); + Assert.equal(uri.asciiHostPort, "[123:45::678]"); + Assert.equal(prin.origin, "http://[123:45::678]"); +} diff --git a/netwerk/test/unit/test_bug1218029.js b/netwerk/test/unit/test_bug1218029.js new file mode 100644 index 0000000000..de49952623 --- /dev/null +++ b/netwerk/test/unit/test_bug1218029.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var tests = [ + { data: "", chunks: [], status: Cr.NS_OK, consume: [], dataChunks: [""] }, + { + data: "TWO-PARTS", + chunks: [4, 5], + status: Cr.NS_OK, + consume: [4, 5], + dataChunks: ["TWO-", "PARTS", ""], + }, + { + data: "TWO-PARTS", + chunks: [4, 5], + status: Cr.NS_OK, + consume: [0, 0], + dataChunks: ["TWO-", "TWO-PARTS", "TWO-PARTS"], + }, + { + data: "3-PARTS", + chunks: [1, 1, 5], + status: Cr.NS_OK, + consume: [0, 2, 5], + dataChunks: ["3", "3-", "PARTS", ""], + }, + { + data: "ALL-AT-ONCE", + chunks: [11], + status: Cr.NS_OK, + consume: [0], + dataChunks: ["ALL-AT-ONCE", "ALL-AT-ONCE"], + }, + { + data: "ALL-AT-ONCE", + chunks: [11], + status: Cr.NS_OK, + consume: [11], + dataChunks: ["ALL-AT-ONCE", ""], + }, + { + data: "ERROR", + chunks: [1], + status: Cr.NS_ERROR_OUT_OF_MEMORY, + consume: [0], + dataChunks: ["E", "E"], + }, +]; + +/** + * @typedef TestData + * @property {string} data - data for the test. + * @property {Array} chunks - lengths of the chunks that are incrementally sent + * to the loader. + * @property {number} status - final status sent on onStopRequest. + * @property {Array} consume - lengths of consumed data that is reported at + * the onIncrementalData callback. + * @property {Array} dataChunks - data chunks that are reported at the + * onIncrementalData and onStreamComplete callbacks. + */ + +function execute_test(test) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = test.data; + + let channel = { + contentLength: -1, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + let chunkIndex = 0; + + let observer = { + onStreamComplete(loader, context, status, length, data) { + equal(chunkIndex, test.dataChunks.length - 1); + var expectedChunk = test.dataChunks[chunkIndex]; + equal(length, expectedChunk.length); + equal(String.fromCharCode.apply(null, data), expectedChunk); + + equal(status, test.status); + }, + onIncrementalData(loader, context, length, data, consumed) { + ok(chunkIndex < test.dataChunks.length - 1); + var expectedChunk = test.dataChunks[chunkIndex]; + equal(length, expectedChunk.length); + equal(String.fromCharCode.apply(null, data), expectedChunk); + + consumed.value = test.consume[chunkIndex]; + chunkIndex++; + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIIncrementalStreamLoaderObserver", + ]), + }; + + let listener = Cc[ + "@mozilla.org/network/incremental-stream-loader;1" + ].createInstance(Ci.nsIIncrementalStreamLoader); + listener.init(observer); + + listener.onStartRequest(channel); + var offset = 0; + test.chunks.forEach(function(chunkLength) { + listener.onDataAvailable(channel, stream, offset, chunkLength); + offset += chunkLength; + }); + listener.onStopRequest(channel, test.status); +} + +function run_test() { + tests.forEach(execute_test); +} diff --git a/netwerk/test/unit/test_bug1279246.js b/netwerk/test/unit/test_bug1279246.js new file mode 100644 index 0000000000..a0a8639cf3 --- /dev/null +++ b/netwerk/test/unit/test_bug1279246.js @@ -0,0 +1,98 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var pass = 0; +var responseBody = [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03]; +var responseLen = 5; +var testUrl = "/test/brotli"; + +function setupChannel() { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + testUrl, + loadUsingSystemPrincipal: true, + }); +} + +function Listener() {} + +Listener.prototype = { + _buffer: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + Assert.equal(request.status, Cr.NS_OK); + this._buffer = ""; + }, + + onDataAvailable(request, stream, offset, cnt) { + if (pass == 0) { + this._buffer = this._buffer.concat(read_stream(stream, cnt)); + } else { + request.QueryInterface(Ci.nsICachingChannel); + if (!request.isFromCache()) { + do_throw("Response is not from the cache"); + } + + request.cancel(Cr.NS_ERROR_ABORT); + } + }, + + onStopRequest(request, status) { + if (pass == 0) { + Assert.equal(this._buffer.length, responseLen); + pass++; + + var channel = setupChannel(); + channel.loadFlags = Ci.nsIRequest.VALIDATE_NEVER; + channel.asyncOpen(new Listener()); + } else { + httpserver.stop(do_test_finished); + prefs.setCharPref("network.http.accept-encoding", cePref); + } + }, +}; + +var prefs; +var cePref; +function run_test() { + do_get_profile(); + + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + cePref = prefs.getCharPref("network.http.accept-encoding"); + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br"); + + // Disable rcwn to make cache behavior deterministic. + prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserver.registerPathHandler(testUrl, handler); + httpserver.start(-1); + + var channel = setupChannel(); + channel.asyncOpen(new Listener()); + + do_test_pending(); +} + +function handler(metadata, response) { + Assert.equal(pass, 0); // the second response must be server from the cache + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", "br", false); + response.setHeader("Content-Length", "" + responseBody.length, false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(responseBody); + response.finish(); +} diff --git a/netwerk/test/unit/test_bug1312774_http1.js b/netwerk/test/unit/test_bug1312774_http1.js new file mode 100644 index 0000000000..97a7f4d1eb --- /dev/null +++ b/netwerk/test/unit/test_bug1312774_http1.js @@ -0,0 +1,153 @@ +// test bug 1312774. +// Create 6 (=network.http.max-persistent-connections-per-server) +// common Http requests and 2 urgent-start Http requests to a single +// host and path, in parallel. +// Let all the requests unanswered by the server handler. (process them +// async and don't finish) +// The first 6 pending common requests will fill the limit for per-server +// parallelism. +// But the two urgent requests must reach the server despite those 6 common +// pending requests. +// The server handler doesn't let the test finish until all 8 expected requests +// arrive. +// Note: if the urgent request handling is broken (the urgent-marked requests +// get blocked by queuing) this test will time out + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var urgentRequests = 0; +var totalRequests = 0; +var debug = false; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function commonHttpRequest(id) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(id); + chan.setRequestHeader("X-ID", id, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create common http request id=" + id); +} + +function urgentStartHttpRequest(id) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(id); + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.UrgentStart); + chan.setRequestHeader("X-ID", id, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create urgent-start http request id=" + id); +} + +function setup_httpRequests() { + log("setup_httpRequests"); + for (var i = 0; i < maxConnections; i++) { + commonHttpRequest(i); + do_test_pending(); + } +} + +function setup_urgentStartRequests() { + for (var i = 0; i < urgentRequests; i++) { + urgentStartHttpRequest(1000 + i); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +var testOrder = 0; +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + maxConnections = prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + urgentRequests = 2; + totalRequests = maxConnections + urgentRequests; + var allCommonHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function(metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + response.processAsync(); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allCommonHttpRequestReceived + ) { + allCommonHttpRequestReceived = true; + setup_urgentStartRequests(); + } + // Wait for all expected requests to come but don't process then. + // Collect them in a queue for later processing. We don't want to + // respond to the client until all the expected requests are made + // to the server. + if (responseQueue.length == maxConnections + urgentRequests) { + processResponse(); + } + }); + + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function processResponse() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + setup_http_server(); + setup_httpRequests(); +} diff --git a/netwerk/test/unit/test_bug1312782_http1.js b/netwerk/test/unit/test_bug1312782_http1.js new file mode 100644 index 0000000000..4c13c2b4ab --- /dev/null +++ b/netwerk/test/unit/test_bug1312782_http1.js @@ -0,0 +1,206 @@ +// test bug 1312782. +// +// Summary: +// Assume we have 6 http requests in queue, 4 are from the focused window and +// the other 2 are from the non-focused window. We want to test that the server +// should receive 4 requests from the focused window first and then receive the +// rest 2 requests. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until get +// all 6 requests. +// 2. Once server receive 6 dummy requests, create 4 http requests with the focused +// window id and 2 requests with non-focused window id. Note that the requets's +// id is a serial number starting from the focused window id. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests again and wait +// until get all 6 requests. +// 4. When the server receive all 6 requests, starts to check that the request ids of +// the first 4 requests in the queue should be all less than focused window id +// plus 4. Also, the request ids of the rest requests should be less than non-focused +// window id + 2. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +const FOCUSED_WINDOW_ID = 123; +var NON_FOCUSED_WINDOW_ID; +var FOCUSED_WINDOW_REQUEST_COUNT; +var NON_FOCUSED_WINDOW_REQUEST_COUNT; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(windowId, requestId) { + let uri = baseURL; + var chan = make_channel(uri); + chan.topLevelOuterContentWindowId = windowId; + var listner = new HttpResponseListener(requestId); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests() { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(0, i); + do_test_pending(); + } +} + +function setup_focusedWindowHttpRequests() { + log("setup_focusedWindowHttpRequests"); + for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest(FOCUSED_WINDOW_ID, FOCUSED_WINDOW_ID + i); + do_test_pending(); + } +} + +function setup_nonFocusedWindowHttpRequests() { + log("setup_nonFocusedWindowHttpRequests"); + for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest(NON_FOCUSED_WINDOW_ID, NON_FOCUSED_WINDOW_ID + i); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +function check_response_id(responses, maxWindowId) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + log("response id=" + id + " maxWindowId=" + maxWindowId); + Assert.ok(id < maxWindowId); + } +} + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + maxConnections = prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.8); + NON_FOCUSED_WINDOW_REQUEST_COUNT = + maxConnections - FOCUSED_WINDOW_REQUEST_COUNT; + NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT; + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function(metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allDummyHttpRequestReceived + ) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_nonFocusedWindowHttpRequests(); + setup_focusedWindowHttpRequests(); + processResponses(); + } else if (responseQueue.length == maxConnections) { + var focusedWindowResponses = responseQueue.slice( + 0, + FOCUSED_WINDOW_REQUEST_COUNT + ); + var nonFocusedWindowResponses = responseQueue.slice( + FOCUSED_WINDOW_REQUEST_COUNT, + responseQueue.length + ); + check_response_id( + focusedWindowResponses, + FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT + ); + check_response_id( + nonFocusedWindowResponses, + NON_FOCUSED_WINDOW_ID + NON_FOCUSED_WINDOW_REQUEST_COUNT + ); + processResponses(); + } + }); + + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + // Make sure "network.http.active_tab_priority" is true, so we can expect to + // receive http requests with focused window id before others. + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setBoolPref("network.http.active_tab_priority", true); + + setup_http_server(); + setup_dummyHttpRequests(); + + var windowIdWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance( + Ci.nsISupportsPRUint64 + ); + windowIdWrapper.data = FOCUSED_WINDOW_ID; + var obsvc = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + obsvc.notifyObservers( + windowIdWrapper, + "net:current-toplevel-outer-content-windowid" + ); +} diff --git a/netwerk/test/unit/test_bug1355539_http1.js b/netwerk/test/unit/test_bug1355539_http1.js new file mode 100644 index 0000000000..55bc890d66 --- /dev/null +++ b/netwerk/test/unit/test_bug1355539_http1.js @@ -0,0 +1,207 @@ +// test bug 1355539. +// +// Summary: +// Transactions in one pending queue are splited into two groups: +// [(Blocking Group)|(Non Blocking Group)] +// In each group, the transactions are ordered by its priority. +// This test will check if the transaction's order in pending queue is correct. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until get +// all 6 requests. +// 2. Once server receive 6 dummy requests, create another 6 http requests with the +// defined priority and class flag in |transactionQueue|. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests and put them in +// |responseQueue|. +// 4. When the server receive all 6 requests, check if the order in |responseQueue| is +// equal to |transactionQueue| by comparing the value of X-ID. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +var dummyResponseQueue = []; +var responseQueue = []; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(requestId, priority, isBlocking, callback) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(requestId, callback); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.QueryInterface(Ci.nsISupportsPriority).priority = priority; + if (isBlocking) { + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.Leader); + } + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests(callback) { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(i, i, false, callback); + do_test_pending(); + } +} + +var transactionQueue = [ + { + requestId: 101, + priority: Ci.nsISupportsPriority.PRIORITY_HIGH, + isBlocking: true, + }, + { + requestId: 102, + priority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + isBlocking: true, + }, + { + requestId: 103, + priority: Ci.nsISupportsPriority.PRIORITY_LOW, + isBlocking: true, + }, + { + requestId: 104, + priority: Ci.nsISupportsPriority.PRIORITY_HIGH, + isBlocking: false, + }, + { + requestId: 105, + priority: Ci.nsISupportsPriority.PRIORITY_NORMAL, + isBlocking: false, + }, + { + requestId: 106, + priority: Ci.nsISupportsPriority.PRIORITY_LOW, + isBlocking: false, + }, +]; + +function setup_HttpRequests() { + log("setup_HttpRequests"); + // Create channels in reverse order + for (var i = transactionQueue.length - 1; i > -1; ) { + var e = transactionQueue[i]; + createHttpRequest(e.requestId, e.priority, e.isBlocking); + do_test_pending(); + --i; + } +} + +function check_response_id(responses) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + Assert.equal(id, transactionQueue[i].requestId); + } +} + +function HttpResponseListener(id, onStopCallback) { + this.id = id; + this.stopCallback = onStopCallback; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + if (this.stopCallback) { + this.stopCallback(); + } + }, +}; + +function setup_http_server() { + log("setup_http_server"); + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + maxConnections = prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function(metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + + if (!allDummyHttpRequestReceived) { + dummyResponseQueue.push(response); + } else { + responseQueue.push(response); + } + + if (dummyResponseQueue.length == maxConnections) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_HttpRequests(); + processDummyResponse(); + } else if (responseQueue.length == maxConnections) { + log("received all http requets"); + check_response_id(responseQueue); + processResponses(); + } + }); + + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function processDummyResponse() { + if (!dummyResponseQueue.length) { + return; + } + var resposne = dummyResponseQueue.pop(); + resposne.finish(); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + setup_http_server(); + setup_dummyHttpRequests(processDummyResponse); +} diff --git a/netwerk/test/unit/test_bug1378385_http1.js b/netwerk/test/unit/test_bug1378385_http1.js new file mode 100644 index 0000000000..e14a9e6a99 --- /dev/null +++ b/netwerk/test/unit/test_bug1378385_http1.js @@ -0,0 +1,202 @@ +// test bug 1378385. +// +// Summary: +// Assume we have 6 http requests in queue, 3 are from the focused window with +// normal priority and the other 3 are from the non-focused window with the +// highest priority. +// We want to test that when "network.http.active_tab_priority" is false, +// the server should receive 3 requests with the highest priority first +// and then receive the rest 3 requests. +// +// Test step: +// 1. Create 6 dummy http requests. Server would not process responses until told +// all 6 requests. +// 2. Once server receive 6 dummy requests, create 3 http requests with the focused +// window id and normal priority and 3 requests with non-focused window id and +// the highrst priority. +// Note that the requets's id is set to its window id. +// 3. Server starts to process the 6 dummy http requests, so the client can start to +// process the pending queue. Server will queue those http requests again and wait +// until get all 6 requests. +// 4. When the server receive all 6 requests, we want to check if 3 requests with higher +// priority are sent before others. +// First, we check that if the request id of the first 3 requests in the queue is +// equal to non focused window id. +// Second, we check if the request id of the rest requests is equal to focused +// window id. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +const FOCUSED_WINDOW_ID = 123; +var NON_FOCUSED_WINDOW_ID; +var FOCUSED_WINDOW_REQUEST_COUNT; +var NON_FOCUSED_WINDOW_REQUEST_COUNT; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(windowId, requestId, priority) { + let uri = baseURL; + var chan = make_channel(uri); + chan.topLevelOuterContentWindowId = windowId; + chan.QueryInterface(Ci.nsISupportsPriority).priority = priority; + var listner = new HttpResponseListener(requestId); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setup_dummyHttpRequests() { + log("setup_dummyHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(0, i, Ci.nsISupportsPriority.PRIORITY_NORMAL); + do_test_pending(); + } +} + +function setup_focusedWindowHttpRequests() { + log("setup_focusedWindowHttpRequests"); + for (var i = 0; i < FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest( + FOCUSED_WINDOW_ID, + FOCUSED_WINDOW_ID, + Ci.nsISupportsPriority.PRIORITY_NORMAL + ); + do_test_pending(); + } +} + +function setup_nonFocusedWindowHttpRequests() { + log("setup_nonFocusedWindowHttpRequests"); + for (var i = 0; i < NON_FOCUSED_WINDOW_REQUEST_COUNT; i++) { + createHttpRequest( + NON_FOCUSED_WINDOW_ID, + NON_FOCUSED_WINDOW_ID, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + do_test_pending(); + } +} + +function HttpResponseListener(id) { + this.id = id; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id); + do_test_finished(); + }, +}; + +function check_response_id(responses, windowId) { + for (var i = 0; i < responses.length; i++) { + var id = responses[i].getHeader("X-ID"); + log("response id=" + id + " windowId=" + windowId); + Assert.equal(id, windowId); + } +} + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + maxConnections = prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + FOCUSED_WINDOW_REQUEST_COUNT = Math.floor(maxConnections * 0.5); + NON_FOCUSED_WINDOW_REQUEST_COUNT = + maxConnections - FOCUSED_WINDOW_REQUEST_COUNT; + NON_FOCUSED_WINDOW_ID = FOCUSED_WINDOW_ID + FOCUSED_WINDOW_REQUEST_COUNT; + + var allDummyHttpRequestReceived = false; + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function(metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if ( + responseQueue.length == maxConnections && + !allDummyHttpRequestReceived + ) { + log("received all dummy http requets"); + allDummyHttpRequestReceived = true; + setup_nonFocusedWindowHttpRequests(); + setup_focusedWindowHttpRequests(); + processResponses(); + } else if (responseQueue.length == maxConnections) { + var nonFocusedWindowResponses = responseQueue.slice( + 0, + NON_FOCUSED_WINDOW_REQUEST_COUNT + ); + var focusedWindowResponses = responseQueue.slice( + NON_FOCUSED_WINDOW_REQUEST_COUNT, + responseQueue.length + ); + check_response_id(nonFocusedWindowResponses, NON_FOCUSED_WINDOW_ID); + check_response_id(focusedWindowResponses, FOCUSED_WINDOW_ID); + processResponses(); + } + }); + + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function processResponses() { + while (responseQueue.length) { + var resposne = responseQueue.pop(); + resposne.finish(); + } +} + +function run_test() { + // Set "network.http.active_tab_priority" to false, so we can expect to + // receive http requests with higher priority first. + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setBoolPref("network.http.active_tab_priority", false); + + setup_http_server(); + setup_dummyHttpRequests(); +} diff --git a/netwerk/test/unit/test_bug1411316_http1.js b/netwerk/test/unit/test_bug1411316_http1.js new file mode 100644 index 0000000000..e41615688e --- /dev/null +++ b/netwerk/test/unit/test_bug1411316_http1.js @@ -0,0 +1,117 @@ +// Test bug 1411316. +// +// Summary: +// The purpose of this test is to test whether the HttpConnectionMgr really +// cancel and close all connecitons when get "net:cancel-all-connections". +// +// Test step: +// 1. Create 6 http requests. Server would not process responses and just put +// all requests in its queue. +// 2. Once server receive all 6 requests, call notifyObservers with the +// topic "net:cancel-all-connections". +// 3. We expect that all 6 active connections should be closed with the status +// NS_ERROR_ABORT. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server = new HttpServer(); +server.start(-1); +var baseURL = "http://localhost:" + server.identity.primaryPort + "/"; +var maxConnections = 0; +var debug = false; +var requestId = 0; + +function log(msg) { + if (!debug) { + return; + } + + if (msg) { + dump("TEST INFO | " + msg + "\n"); + } +} + +function make_channel(url) { + var request = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }); + request.QueryInterface(Ci.nsIHttpChannel); + return request; +} + +function serverStopListener() { + server.stop(); +} + +function createHttpRequest(status) { + let uri = baseURL; + var chan = make_channel(uri); + var listner = new HttpResponseListener(++requestId, status); + chan.setRequestHeader("X-ID", requestId, false); + chan.setRequestHeader("Cache-control", "no-store", false); + chan.asyncOpen(listner); + log("Create http request id=" + requestId); +} + +function setupHttpRequests(status) { + log("setupHttpRequests"); + for (var i = 0; i < maxConnections; i++) { + createHttpRequest(status); + do_test_pending(); + } +} + +function HttpResponseListener(id, onStopRequestStatus) { + this.id = id; + this.onStopRequestStatus = onStopRequestStatus; +} + +HttpResponseListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream, off, cnt) {}, + + onStopRequest(request, status) { + log("STOP id=" + this.id + " status=" + status); + Assert.ok(this.onStopRequestStatus == status); + do_test_finished(); + }, +}; + +var responseQueue = []; +function setup_http_server() { + log("setup_http_server"); + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + maxConnections = prefs.getIntPref( + "network.http.max-persistent-connections-per-server" + ); + + // Start server; will be stopped at test cleanup time. + server.registerPathHandler("/", function(metadata, response) { + var id = metadata.getHeader("X-ID"); + log("Server recived the response id=" + id); + + response.processAsync(); + response.setHeader("X-ID", id); + responseQueue.push(response); + + if (responseQueue.length == maxConnections) { + log("received all http requets"); + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + } + }); + + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function run_test() { + setup_http_server(); + setupHttpRequests(Cr.NS_ERROR_ABORT); +} diff --git a/netwerk/test/unit/test_bug1527293.js b/netwerk/test/unit/test_bug1527293.js new file mode 100644 index 0000000000..9f4ab24008 --- /dev/null +++ b/netwerk/test/unit/test_bug1527293.js @@ -0,0 +1,92 @@ +// Test bug 1527293 +// +// Summary: +// The purpose of this test is to check that a cache entry is doomed and not +// reused when we don't write the content due to max entry size limit. +// +// Test step: +// 1. Create http request for an entry whose size is bigger than we allow to +// cache. The response must contain Content-Range header so the content size +// is known in advance, but it must not contain Content-Length header because +// the bug isn't reproducible with it. +// 2. After receiving and checking the content do the same request again. +// 3. Check that the request isn't conditional, i.e. the entry from previous +// load was doomed. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + + Assert.throws( + () => { + metadata.getHeader("If-None-Match"); + }, + /NS_ERROR_NOT_AVAILABLE/, + "conditional request not expected" + ); + + response.setHeader("Accept-Ranges", "bytes"); + let len = responseBody.length; + response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(firstTimeThrough, null)); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + + var chan = make_channel(URL + "/content"); + chan.asyncOpen(new ChannelListener(secondTimeThrough, null)); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_bug1683176.js b/netwerk/test/unit/test_bug1683176.js new file mode 100644 index 0000000000..16b1ff4a3f --- /dev/null +++ b/netwerk/test/unit/test_bug1683176.js @@ -0,0 +1,104 @@ +// Test bug 1683176 +// +// Summary: +// Test the case when a channel is cancelled when "negotiate" authentication +// is processing. +// + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let httpserv; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +XPCOMUtils.defineLazyGetter(this, "PORT", function() { + return httpserv.identity.primaryPort; +}); + +function makeChan(url, loadingUrl) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var principal = ssm.createContentPrincipal(ios.newURI(loadingUrl), {}); + return NetUtil.newChannel({ + uri: url, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); +} + +function authHandler(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 401 Unauthorized\r\n"); + response.write("WWW-Authenticate: Negotiate\r\n"); + response.write("WWW-Authenticate: Basic realm=test\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function setup() { + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + prefs.setStringPref("network.negotiate-auth.trusted-uris", "localhost"); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/auth", authHandler); + httpserv.start(-1); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.auth.subresource-http-auth-allow"); + prefs.clearUserPref("network.negotiate-auth.trusted-uris"); + await httpserv.stop(); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + let topic = "http-on-transaction-suspended-authentication"; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.cancel(Cr.NS_BINDING_ABORTED); + resolve(); + } + }, + }; + Services.obs.addObserver(observer, topic); + + chan.asyncOpen(new ChannelListener(finish, null, CL_EXPECT_FAILURE)); + function finish() { + resolve(); + } + }); +} + +add_task(async function testCancelAuthentication() { + let chan = makeChan(URL + "/auth", URL); + await channelOpenPromise(chan); + Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); +}); diff --git a/netwerk/test/unit/test_bug203271.js b/netwerk/test/unit/test_bug203271.js new file mode 100644 index 0000000000..9801d5c5d4 --- /dev/null +++ b/netwerk/test/unit/test_bug203271.js @@ -0,0 +1,248 @@ +// +// Tests if a response with an Expires-header in the past +// and Cache-Control: max-age in the future works as +// specified in RFC 2616 section 14.9.3 by letting max-age +// take precedence + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const BUGID = "203271"; + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + // original problem described in bug#203271 + { + url: "/precedence", + server: "0", + expected: "0", + responseheader: [ + "Expires: " + getDateString(-1), + "Cache-Control: max-age=3600", + ], + }, + + { + url: "/precedence?0", + server: "0", + expected: "0", + responseheader: [ + "Cache-Control: max-age=3600", + "Expires: " + getDateString(-1), + ], + }, + + // max-age=1s, expires=1 year from now + { + url: "/precedence?1", + server: "0", + expected: "0", + responseheader: [ + "Expires: " + getDateString(1), + "Cache-Control: max-age=1", + ], + }, + + // expires=now + { + url: "/precedence?2", + server: "0", + expected: "0", + responseheader: ["Expires: " + getDateString(0)], + }, + + // max-age=1s + { + url: "/precedence?3", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=1"], + }, + + // The test below is the example from + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=203271#c27 + // + // max-age=2592000s (1 month), expires=1 year from now, date=1 year ago + { + url: "/precedence?4", + server: "0", + expected: "0", + responseheader: [ + "Cache-Control: private, max-age=2592000", + "Expires: " + getDateString(+1), + ], + explicitDate: getDateString(-1), + }, + + // The two tests below are also examples of clocks really out of synch + // max-age=1s, date=1 year from now + { + url: "/precedence?5", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=1"], + explicitDate: getDateString(1), + }, + + // max-age=60s, date=1 year from now + { + url: "/precedence?6", + server: "0", + expected: "0", + responseheader: ["Cache-Control: max-age=60"], + explicitDate: getDateString(1), + }, + + // this is just to get a pause of 3s to allow cache-entries to expire + { url: "/precedence?999", server: "0", expected: "0", delay: "3000" }, + + // Below are the cases which actually matters + { url: "/precedence", server: "1", expected: "0" }, // should be cached + + { url: "/precedence?0", server: "1", expected: "0" }, // should be cached + + { url: "/precedence?1", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?2", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?3", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?4", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?5", server: "1", expected: "1" }, // should have expired + + { url: "/precedence?6", server: "1", expected: "0" }, // should be cached +]; + +function logit(i, data, ctx) { + dump( + "requested [" + + tests[i].server + + "] " + + "got [" + + data + + "] " + + "expected [" + + tests[i].expected + + "]" + ); + + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); + // Dump all response-headers + dump("\n===================================\n"); + ctx.visitResponseHeaders({ + visitHeader(key, val) { + dump("\t" + key + ":" + val + "\n"); + }, + }); + dump("===================================\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; // default value, just being paranoid... + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, channel)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data, ctx); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + var delay = tests[index++].delay; + if (delay) { + do_timeout(delay, triggerNextTest); + } else { + triggerNextTest(); + } + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/precedence", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + + var date = tests[index].explicitDate; + if (date == undefined) { + response.setHeader("Date", getDateString(0), false); + } else { + response.setHeader("Date", date, false); + } + + var header = tests[index].responseheader; + if (header == undefined) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + for (var i = 0; i < header.length; i++) { + var splitHdr = header[i].split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug248970_cache.js b/netwerk/test/unit/test_bug248970_cache.js new file mode 100644 index 0000000000..2b7521565d --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cache.js @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// names for cache devices +const kDiskDevice = "disk"; +const kMemoryDevice = "memory"; +const kOfflineDevice = "appcache"; + +const kCacheA = "http://cache/A"; +const kCacheA2 = "http://cache/A2"; +const kCacheB = "http://cache/B"; +const kCacheC = "http://cache/C"; +const kTestContent = "test content"; + +function make_input_stream_scriptable(input) { + var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + wrapper.init(input); + return wrapper; +} + +const entries = [ + // key content device should exist after leaving PB + [kCacheA, kTestContent, kMemoryDevice, true], + [kCacheA2, kTestContent, kDiskDevice, false], + [kCacheB, kTestContent, kDiskDevice, true], + [kCacheC, kTestContent, kOfflineDevice, true], +]; + +var store_idx; +var store_cb = null; +var appCache = null; + +function store_entries(cb) { + if (cb) { + store_cb = cb; + store_idx = 0; + } + + if (store_idx == entries.length) { + executeSoon(store_cb); + return; + } + + asyncOpenCacheEntry( + entries[store_idx][0], + entries[store_idx][2], + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.custom(false, { + privateBrowsingId: entries[store_idx][3] ? 0 : 1, + }), + store_data, + appCache + ); +} + +var store_data = function(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(0, entries[store_idx][1].length); + + var written = os.write(entries[store_idx][1], entries[store_idx][1].length); + if (written != entries[store_idx][1].length) { + do_throw( + "os.write has not written all data!\n" + + " Expected: " + + entries[store_idx][1].length + + "\n" + + " Actual: " + + written + + "\n" + ); + } + os.close(); + entry.close(); + store_idx++; + executeSoon(store_entries); +}; + +var check_idx; +var check_cb = null; +var check_pb_exited; +function check_entries(cb, pbExited) { + if (cb) { + check_cb = cb; + check_idx = 0; + check_pb_exited = pbExited; + } + + if (check_idx == entries.length) { + executeSoon(check_cb); + return; + } + + asyncOpenCacheEntry( + entries[check_idx][0], + entries[check_idx][2], + Ci.nsICacheStorage.OPEN_READONLY, + Services.loadContextInfo.custom(false, { + privateBrowsingId: entries[check_idx][3] ? 0 : 1, + }), + check_data, + appCache + ); +} + +var check_data = function(status, entry) { + var cont = function() { + check_idx++; + executeSoon(check_entries); + }; + + if (!check_pb_exited || entries[check_idx][3]) { + Assert.equal(status, Cr.NS_OK); + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + entry.close(); + Assert.equal(read, entries[check_idx][1]); + cont(); + }); + } else { + Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); + cont(); + } +}; + +function run_test() { + // Simulate a profile dir for xpcshell + do_get_profile(); + + Services.prefs.setBoolPref("browser.cache.offline.enable", true); + Services.prefs.setBoolPref("browser.cache.offline.storage.enable", true); + + appCache = Cc["@mozilla.org/network/application-cache-service;1"] + .getService(Ci.nsIApplicationCacheService) + .getApplicationCache("fake-client-id|fake-group-id"); + + // Start off with an empty cache + evict_cache_entries(); + + // Store cache-A, cache-A2, cache-B and cache-C + store_entries(run_test2); + + do_test_pending(); +} + +function run_test2() { + // Check if cache-A, cache-A2, cache-B and cache-C are available + check_entries(run_test3, false); +} + +function run_test3() { + // Simulate all private browsing instances being closed + var obsvc = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + obsvc.notifyObservers(null, "last-pb-context-exited"); + + // Make sure the memory device is not empty + get_device_entry_count(kMemoryDevice, null, function(count) { + Assert.equal(count, 1); + // Check if cache-A is gone, and cache-B and cache-C are still available + check_entries(do_test_finished, true); + }); +} diff --git a/netwerk/test/unit/test_bug248970_cookie.js b/netwerk/test/unit/test_bug248970_cookie.js new file mode 100644 index 0000000000..d7f3b381a5 --- /dev/null +++ b/netwerk/test/unit/test_bug248970_cookie.js @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver; + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} +function makeChan(path) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/" + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function setup_chan(path, isPrivate, callback) { + var chan = makeChan(path); + chan.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(isPrivate); + chan.asyncOpen(new ChannelListener(callback)); +} + +function set_cookie(value, callback) { + return setup_chan("set?cookie=" + value, false, callback); +} + +function set_private_cookie(value, callback) { + return setup_chan("set?cookie=" + value, true, callback); +} + +function check_cookie_presence(value, isPrivate, expected, callback) { + setup_chan("present?cookie=" + value.replace("=", "|"), isPrivate, function( + req + ) { + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.responseStatus, expected ? 200 : 404); + callback(req); + }); +} + +function presentHandler(metadata, response) { + var present = false; + var match = /cookie=([^&]*)/.exec(metadata.queryString); + if (match) { + try { + present = metadata + .getHeader("Cookie") + .includes(match[1].replace("|", "=")); + } catch (x) {} + } + response.setStatusLine("1.0", present ? 200 : 404, ""); +} + +function setHandler(metadata, response) { + response.setStatusLine("1.0", 200, "Cookie set"); + var match = /cookie=([^&]*)/.exec(metadata.queryString); + if (match) { + response.setHeader("Set-Cookie", match[1]); + } +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/set", setHandler); + httpserver.registerPathHandler("/present", presentHandler); + httpserver.start(-1); + + do_test_pending(); + + function check_cookie(req) { + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.responseStatus, 200); + try { + Assert.ok( + req.getResponseHeader("Set-Cookie") != "", + "expected a Set-Cookie header" + ); + } catch (x) { + do_throw("missing Set-Cookie header"); + } + + runNextTest(); + } + + let tests = []; + + function runNextTest() { + executeSoon(tests.shift()); + } + + tests.push(function() { + set_cookie("C1=V1", check_cookie); + }); + tests.push(function() { + set_private_cookie("C2=V2", check_cookie); + }); + tests.push(function() { + // Check that the first cookie is present in a non-private request + check_cookie_presence("C1=V1", false, true, runNextTest); + }); + tests.push(function() { + // Check that the second cookie is present in a private request + check_cookie_presence("C2=V2", true, true, runNextTest); + }); + tests.push(function() { + // Check that the first cookie is not present in a private request + check_cookie_presence("C1=V1", true, false, runNextTest); + }); + tests.push(function() { + // Check that the second cookie is not present in a non-private request + check_cookie_presence("C2=V2", false, false, runNextTest); + }); + + // The following test only works in a non-e10s situation at the moment, + // since the notification needs to run in the parent process but there is + // no existing mechanism to make that happen. + if (!inChildProcess()) { + tests.push(function() { + // Simulate all private browsing instances being closed + var obsvc = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + obsvc.notifyObservers(null, "last-pb-context-exited"); + // Check that all private cookies are now unavailable in new private requests + check_cookie_presence("C2=V2", true, false, runNextTest); + }); + } + + tests.push(function() { + httpserver.stop(do_test_finished); + }); + + runNextTest(); +} diff --git a/netwerk/test/unit/test_bug261425.js b/netwerk/test/unit/test_bug261425.js new file mode 100644 index 0000000000..3da3ba610f --- /dev/null +++ b/netwerk/test/unit/test_bug261425.js @@ -0,0 +1,37 @@ +"use strict"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var newURI = ios.newURI("http://foo.com"); + + var success = false; + try { + newURI = newURI + .mutate() + .setSpec("http: //foo.com") + .finalize(); + } catch (e) { + success = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (!success) { + do_throw( + "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!" + ); + } + + success = false; + try { + newURI + .mutate() + .setHost(" foo.com") + .finalize(); + } catch (e) { + success = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (!success) { + do_throw( + "We didn't throw NS_ERROR_MALFORMED_URI when a space was passed in the hostname!" + ); + } +} diff --git a/netwerk/test/unit/test_bug263127.js b/netwerk/test/unit/test_bug263127.js new file mode 100644 index 0000000000..d829bbd800 --- /dev/null +++ b/netwerk/test/unit/test_bug263127.js @@ -0,0 +1,58 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server; +const BUGID = "263127"; + +var listener = { + QueryInterface: ChromeUtils.generateQI(["nsIDownloadObserver"]), + + onDownloadComplete(downloader, request, ctxt, status, file) { + do_test_pending(); + server.stop(do_test_finished); + + if (!file) { + do_throw("Download failed"); + } + + try { + file.remove(false); + } catch (e) { + do_throw(e); + } + + Assert.ok(!file.exists()); + + do_test_finished(); + }, +}; + +function run_test() { + // start server + server = new HttpServer(); + server.start(-1); + + // Initialize downloader + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }); + var targetFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + targetFile.append("bug" + BUGID + ".test"); + if (targetFile.exists()) { + targetFile.remove(false); + } + + var downloader = Cc["@mozilla.org/network/downloader;1"].createInstance( + Ci.nsIDownloader + ); + downloader.init(listener, targetFile); + + // Start download + channel.asyncOpen(downloader); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug282432.js b/netwerk/test/unit/test_bug282432.js new file mode 100644 index 0000000000..7794408523 --- /dev/null +++ b/netwerk/test/unit/test_bug282432.js @@ -0,0 +1,38 @@ +"use strict"; + +function run_test() { + do_test_pending(); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + // Make sure we can catch the error NS_ERROR_FILE_NOT_FOUND here. + Assert.equal(aStatusCode, Cr.NS_ERROR_FILE_NOT_FOUND); + do_test_finished(); + }, + + onDataAvailable(aRequest, aStream, aOffset, aCount) { + do_throw("The channel must not call onDataAvailable()."); + }, + }; + + let listener = new StreamListener(); + let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + // This file does not exist. + let file = do_get_file("_NOT_EXIST_.txt", true); + Assert.ok(!file.exists()); + let channel = NetUtil.newChannel({ + uri: ios.newFileURI(file), + loadUsingSystemPrincipal: true, + }); + channel.asyncOpen(listener); +} diff --git a/netwerk/test/unit/test_bug321706.js b/netwerk/test/unit/test_bug321706.js new file mode 100644 index 0000000000..86d8bdc373 --- /dev/null +++ b/netwerk/test/unit/test_bug321706.js @@ -0,0 +1,12 @@ +"use strict"; + +const url = "http://foo.com/folder/file?/."; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var newURI = ios.newURI(url); + Assert.equal(newURI.spec, url); + Assert.equal(newURI.pathQueryRef, "/folder/file?/."); + Assert.equal(newURI.resolve("./file?/."), url); +} diff --git a/netwerk/test/unit/test_bug331825.js b/netwerk/test/unit/test_bug331825.js new file mode 100644 index 0000000000..acb6d7690a --- /dev/null +++ b/netwerk/test/unit/test_bug331825.js @@ -0,0 +1,41 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var server; +const BUGID = "331825"; + +function TestListener() {} +TestListener.prototype.onStartRequest = function(request) {}; +TestListener.prototype.onStopRequest = function(request, status) { + var channel = request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(channel.responseStatus, 304); + + server.stop(do_test_finished); +}; + +function run_test() { + // start server + server = new HttpServer(); + + server.registerPathHandler("/bug" + BUGID, bug331825); + + server.start(-1); + + // make request + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID, + loadUsingSystemPrincipal: true, + }); + + channel.QueryInterface(Ci.nsIHttpChannel); + channel.setRequestHeader("If-None-Match", "foobar", false); + channel.asyncOpen(new TestListener()); + + do_test_pending(); +} + +// PATH HANDLER FOR /bug331825 +function bug331825(metadata, response) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); +} diff --git a/netwerk/test/unit/test_bug336501.js b/netwerk/test/unit/test_bug336501.js new file mode 100644 index 0000000000..3e15fd42bb --- /dev/null +++ b/netwerk/test/unit/test_bug336501.js @@ -0,0 +1,26 @@ +"use strict"; + +function run_test() { + var f = do_get_file("test_bug336501.js"); + + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init(f, -1, -1, 0); + + var bis = Cc["@mozilla.org/network/buffered-input-stream;1"].createInstance( + Ci.nsIBufferedInputStream + ); + bis.init(fis, 32); + + var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(bis); + + sis.read(45); + sis.close(); + + var data = sis.read(45); + Assert.equal(data.length, 0); +} diff --git a/netwerk/test/unit/test_bug337744.js b/netwerk/test/unit/test_bug337744.js new file mode 100644 index 0000000000..583627f270 --- /dev/null +++ b/netwerk/test/unit/test_bug337744.js @@ -0,0 +1,126 @@ +/* verify that certain invalid URIs are not parsed by the resource + protocol handler */ + +"use strict"; + +const specs = [ + "resource://res-test//", + "resource://res-test/?foo=http:", + "resource://res-test/?foo=" + encodeURIComponent("http://example.com/"), + "resource://res-test/?foo=" + encodeURIComponent("x\\y"), + "resource://res-test/..%2F", + "resource://res-test/..%2f", + "resource://res-test/..%2F..", + "resource://res-test/..%2f..", + "resource://res-test/../../", + "resource://res-test/http://www.mozilla.org/", + "resource://res-test/file:///", +]; + +const error_specs = [ + "resource://res-test/..\\", + "resource://res-test/..\\..\\", + "resource://res-test/..%5C", + "resource://res-test/..%5c", +]; + +// Create some fake principal that has not enough +// privileges to access any resource: uri. +var uri = NetUtil.newURI("http://www.example.com"); +var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + +function get_channel(spec) { + var channel = NetUtil.newChannel({ + uri: NetUtil.newURI(spec), + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + Assert.throws( + () => { + channel.asyncOpen(null); + }, + /NS_ERROR_DOM_BAD_URI/, + `asyncOpen() of uri: ${spec} should throw` + ); + Assert.throws( + () => { + channel.open(); + }, + /NS_ERROR_DOM_BAD_URI/, + `Open() of uri: ${spec} should throw` + ); + + return channel; +} + +function check_safe_resolution(spec, rootURI) { + info(`Testing URL "${spec}"`); + + let channel = get_channel(spec); + + ok( + channel.name.startsWith(rootURI), + `URL resolved safely to ${channel.name}` + ); + let startOfQuery = channel.name.indexOf("?"); + if (startOfQuery == -1) { + ok(!/%2f/i.test(channel.name), `URL contains no escaped / characters`); + } else { + // Escaped slashes are allowed in the query or hash part of the URL + ok( + !channel.name.replace(/\?.*/, "").includes("%2f"), + `URL contains no escaped slashes before the query ${channel.name}` + ); + } +} + +function check_resolution_error(spec) { + Assert.throws( + () => { + get_channel(spec); + }, + /NS_ERROR_MALFORMED_URI/, + "Expected a malformed URI error" + ); +} + +function run_test() { + // resource:/// and resource://gre/ are resolved specially, so we need + // to create a temporary resource package to test the standard logic + // with. + + let resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService( + Ci.nsIResProtocolHandler + ); + let rootFile = Services.dirsvc.get("GreD", Ci.nsIFile); + let rootURI = Services.io.newFileURI(rootFile); + + rootFile.append("directory-that-does-not-exist"); + let inexistentURI = Services.io.newFileURI(rootFile); + + resProto.setSubstitution("res-test", rootURI); + resProto.setSubstitution("res-inexistent", inexistentURI); + registerCleanupFunction(() => { + resProto.setSubstitution("res-test", null); + resProto.setSubstitution("res-inexistent", null); + }); + + let baseRoot = resProto.resolveURI(Services.io.newURI("resource:///")); + let greRoot = resProto.resolveURI(Services.io.newURI("resource://gre/")); + + for (var spec of specs) { + check_safe_resolution(spec, rootURI.spec); + check_safe_resolution( + spec.replace("res-test", "res-inexistent"), + inexistentURI.spec + ); + check_safe_resolution(spec.replace("res-test", ""), baseRoot); + check_safe_resolution(spec.replace("res-test", "gre"), greRoot); + } + + for (var spec of error_specs) { + check_resolution_error(spec); + } +} diff --git a/netwerk/test/unit/test_bug365133.js b/netwerk/test/unit/test_bug365133.js new file mode 100644 index 0000000000..1f5408d301 --- /dev/null +++ b/netwerk/test/unit/test_bug365133.js @@ -0,0 +1,131 @@ +"use strict"; + +const URL = "ftp://localhost/bug365133/"; + +const tests = [ + [ + /* Unix style listing, space at the end of filename */ + "drwxrwxr-x 2 500 500 4096 Jan 01 2000 a \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "a%20" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT DIRECTORY \n', + ], + [ + /* Unix style listing, space at the end of link name */ + "lrwxrwxrwx 1 500 500 2 Jan 01 2000 l -> a \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "l%20" 2 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n', + ], + [ + /* Unix style listing, regular file with " -> " in name */ + "-rw-rw-r-- 1 500 500 0 Jan 01 2000 arrow -> in name \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "arrow%20-%3E%20in%20name%20" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n', + ], + [ + /* Unix style listing, tab at the end of filename */ + "drwxrwxrwx 2 500 500 4096 Jan 01 2000 t \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "t%09" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT DIRECTORY \n', + ], + [ + /* Unix style listing, multiple " -> " in filename */ + "lrwxrwxrwx 1 500 500 26 Jan 01 2000 symlink with arrow -> in name -> file with arrow -> in name\r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "symlink%20with%20arrow%20-%3E%20in%20name" 26 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n', + ], + [ + /* Unix style listing, multiple " -> " in filename, incorrect filesize */ + "lrwxrwxrwx 1 500 500 0 Jan 01 2000 symlink with arrow -> in name -> file with arrow -> in name\r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "symlink%20with%20arrow%20-%3E%20in%20name%20-%3E%20file%20with%20arrow" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT SYMBOLIC-LINK \n', + ], + [ + /* DOS style listing, space at the end of filename, year 1999 */ + "01-01-99 01:00AM 1024 file \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "file%20" 1024 Fri%2C%2001%20Jan%201999%2001%3A00%3A00%20GMT FILE \n', + ], + [ + /* DOS style listing, tab at the end of filename, year 2000 */ + "01-01-00 01:00AM 1024 file \r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "file%09" 1024 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n', + ], +]; + +function checkData(request, data, ctx) { + Assert.equal(tests[0][1], data); + tests.shift(); + executeSoon(next_test); +} + +function storeData() { + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData( + "text/ftp-dir", + "application/http-index-format", + new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL), + null + ); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = tests[0][0]; + + var url = NetUtil.newURI(URL); + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending() { + return this.pending; + }, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, stream, 0, 0); + channel.pending = false; + converter.onStopRequest(channel, null, Cr.NS_OK); +} + +function next_test() { + if (tests.length == 0) { + do_test_finished(); + } else { + storeData(); + } +} + +function run_test() { + executeSoon(next_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug368702.js b/netwerk/test/unit/test_bug368702.js new file mode 100644 index 0000000000..89a5cc4b20 --- /dev/null +++ b/netwerk/test/unit/test_bug368702.js @@ -0,0 +1,153 @@ +"use strict"; + +function run_test() { + var tld = Cc["@mozilla.org/network/effective-tld-service;1"].getService( + Ci.nsIEffectiveTLDService + ); + Assert.equal(tld.getPublicSuffixFromHost("localhost"), "localhost"); + Assert.equal(tld.getPublicSuffixFromHost("localhost."), "localhost."); + Assert.equal(tld.getPublicSuffixFromHost("domain.com"), "com"); + Assert.equal(tld.getPublicSuffixFromHost("domain.com."), "com."); + Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk"), "co.uk"); + Assert.equal(tld.getPublicSuffixFromHost("domain.co.uk."), "co.uk."); + Assert.equal(tld.getPublicSuffixFromHost("co.uk"), "co.uk"); + Assert.equal(tld.getBaseDomainFromHost("domain.co.uk"), "domain.co.uk"); + Assert.equal(tld.getBaseDomainFromHost("domain.co.uk."), "domain.co.uk."); + + try { + tld.getPublicSuffixFromHost(""); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost("domain.co.uk", 1); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost("co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getBaseDomainFromHost(""); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); + } + + try { + tld.getPublicSuffixFromHost("1.2.3.4"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("2010:836B:4179::836B:4179"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("3232235878"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::ffff:192.9.5.5"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::1"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + // Check IP addresses with trailing dot as well, Necko sometimes accepts + // those (depending on operating system, see bug 380543) + try { + tld.getPublicSuffixFromHost("127.0.0.1."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + try { + tld.getPublicSuffixFromHost("::ffff:127.0.0.1."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_HOST_IS_IP_ADDRESS); + } + + // check normalization: output should be consistent with + // nsIURI::GetAsciiHost(), i.e. lowercased and ASCII/ACE encoded + var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + var uri = ioService.newURI("http://b\u00FCcher.co.uk"); + Assert.equal(tld.getBaseDomain(uri), "xn--bcher-kva.co.uk"); + Assert.equal( + tld.getBaseDomainFromHost("b\u00FCcher.co.uk"), + "xn--bcher-kva.co.uk" + ); + Assert.equal(tld.getPublicSuffix(uri), "co.uk"); + Assert.equal(tld.getPublicSuffixFromHost("b\u00FCcher.co.uk"), "co.uk"); + + // check that malformed hosts are rejected as invalid args + try { + tld.getBaseDomainFromHost("domain.co.uk.."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost("domain.co..uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".domain.co.uk"); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost("."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + try { + tld.getBaseDomainFromHost(".."); + do_throw("this should fail"); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_ILLEGAL_VALUE); + } +} diff --git a/netwerk/test/unit/test_bug369787.js b/netwerk/test/unit/test_bug369787.js new file mode 100644 index 0000000000..9a961b0024 --- /dev/null +++ b/netwerk/test/unit/test_bug369787.js @@ -0,0 +1,71 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const BUGID = "369787"; +var server = null; +var channel = null; + +function change_content_type() { + var origType = channel.contentType; + const newType = "x-foo/x-bar"; + channel.contentType = newType; + Assert.equal(channel.contentType, newType); + channel.contentType = origType; + Assert.equal(channel.contentType, origType); +} + +function TestListener() {} +TestListener.prototype.onStartRequest = function(request) { + try { + // request might be different from channel + channel = request.QueryInterface(Ci.nsIChannel); + + change_content_type(); + } catch (ex) { + print(ex); + throw ex; + } +}; +TestListener.prototype.onStopRequest = function(request, status) { + try { + change_content_type(); + } catch (ex) { + print(ex); + // don't re-throw ex to avoid hanging the test + } + + do_timeout(0, after_channel_closed); +}; + +function after_channel_closed() { + try { + change_content_type(); + } finally { + server.stop(do_test_finished); + } +} + +function run_test() { + // start server + server = new HttpServer(); + + server.registerPathHandler("/bug" + BUGID, bug369787); + + server.start(-1); + + // make request + channel = NetUtil.newChannel({ + uri: "http://localhost:" + server.identity.primaryPort + "/bug" + BUGID, + loadUsingSystemPrincipal: true, + }); + channel.QueryInterface(Ci.nsIHttpChannel); + channel.asyncOpen(new TestListener()); + + do_test_pending(); +} + +// PATH HANDLER FOR /bug369787 +function bug369787(metadata, response) { + /* do nothing */ +} diff --git a/netwerk/test/unit/test_bug371473.js b/netwerk/test/unit/test_bug371473.js new file mode 100644 index 0000000000..05ea15aeac --- /dev/null +++ b/netwerk/test/unit/test_bug371473.js @@ -0,0 +1,40 @@ +"use strict"; + +function test_not_too_long() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var spec = "jar:http://example.com/bar.jar!/"; + try { + ios.newURI(spec); + } catch (e) { + do_throw("newURI threw even though it wasn't passed a large nested URI?"); + } +} + +function test_too_long() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var i; + var prefix = "jar:"; + for (i = 0; i < 16; i++) { + prefix = prefix + prefix; + } + var suffix = "!/"; + for (i = 0; i < 16; i++) { + suffix = suffix + suffix; + } + + var spec = prefix + "http://example.com/bar.jar" + suffix; + try { + // The following will produce a recursive call that if + // unchecked would lead to a stack overflow. If we + // do not crash here and thus an exception is caught + // we have passed the test. + ios.newURI(spec); + } catch (e) {} +} + +function run_test() { + test_not_too_long(); + test_too_long(); +} diff --git a/netwerk/test/unit/test_bug376844.js b/netwerk/test/unit/test_bug376844.js new file mode 100644 index 0000000000..3d97d568c5 --- /dev/null +++ b/netwerk/test/unit/test_bug376844.js @@ -0,0 +1,23 @@ +"use strict"; + +const testURLs = [ + ["http://example.com/<", "http://example.com/%3C"], + ["http://example.com/>", "http://example.com/%3E"], + ["http://example.com/'", "http://example.com/'"], + ['http://example.com/"', "http://example.com/%22"], + ["http://example.com/?<", "http://example.com/?%3C"], + ["http://example.com/?>", "http://example.com/?%3E"], + ["http://example.com/?'", "http://example.com/?%27"], + ['http://example.com/?"', "http://example.com/?%22"], +]; + +function run_test() { + var ioServ = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + for (var i = 0; i < testURLs.length; i++) { + var uri = ioServ.newURI(testURLs[i][0]); + Assert.equal(uri.spec, testURLs[i][1]); + } +} diff --git a/netwerk/test/unit/test_bug376865.js b/netwerk/test/unit/test_bug376865.js new file mode 100644 index 0000000000..260dcbf7e6 --- /dev/null +++ b/netwerk/test/unit/test_bug376865.js @@ -0,0 +1,23 @@ +"use strict"; + +function run_test() { + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsISupportsCString + ); + stream.data = "foo bar baz"; + + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + pump.init(stream, 0, 0, false); + + // When we pass a null listener argument too asyncRead we expect it to throw + // instead of crashing. + try { + pump.asyncRead(null); + } catch (e) { + return; + } + + do_throw("asyncRead didn't throw when passed a null listener argument."); +} diff --git a/netwerk/test/unit/test_bug379034.js b/netwerk/test/unit/test_bug379034.js new file mode 100644 index 0000000000..af2f78dd26 --- /dev/null +++ b/netwerk/test/unit/test_bug379034.js @@ -0,0 +1,21 @@ +"use strict"; + +function run_test() { + const ios = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + var base = ios.newURI("http://localhost/bug379034/index.html"); + + var uri = ios.newURI("http:a.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/a.html"); + + uri = ios.newURI("HtTp:b.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/b.html"); + + uri = ios.newURI("https:c.html", null, base); + Assert.equal(uri.spec, "https://c.html/"); + + uri = ios.newURI("./https:d.html", null, base); + Assert.equal(uri.spec, "http://localhost/bug379034/https:d.html"); +} diff --git a/netwerk/test/unit/test_bug380994.js b/netwerk/test/unit/test_bug380994.js new file mode 100644 index 0000000000..01f896a90f --- /dev/null +++ b/netwerk/test/unit/test_bug380994.js @@ -0,0 +1,26 @@ +/* check resource: protocol for traversal problems */ + +"use strict"; + +const specs = [ + "resource:///chrome/../plugins", + "resource:///chrome%2f../plugins", + "resource:///chrome/..%2fplugins", + "resource:///chrome%2f%2e%2e%2fplugins", + "resource:///../../../..", + "resource:///..%2f..%2f..%2f..", + "resource:///%2e%2e", +]; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + for (var spec of specs) { + var uri = ios.newURI(spec); + if (uri.spec.includes("..")) { + do_throw( + "resource: traversal remains: '" + spec + "' ==> '" + uri.spec + "'" + ); + } + } +} diff --git a/netwerk/test/unit/test_bug388281.js b/netwerk/test/unit/test_bug388281.js new file mode 100644 index 0000000000..8fd43c7c90 --- /dev/null +++ b/netwerk/test/unit/test_bug388281.js @@ -0,0 +1,42 @@ +"use strict"; + +function run_test() { + const ios = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + var uri = ios.newURI("http://foo.com/file.txt"); + uri = uri + .mutate() + .setPort(90) + .finalize(); + Assert.equal(uri.hostPort, "foo.com:90"); + + uri = ios.newURI("http://foo.com:10/file.txt"); + uri = uri + .mutate() + .setPort(500) + .finalize(); + Assert.equal(uri.hostPort, "foo.com:500"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri + .mutate() + .setPort(20) + .finalize(); + Assert.equal(uri.hostPort, "foo.com:20"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri + .mutate() + .setPort(-1) + .finalize(); + Assert.equal(uri.hostPort, "foo.com"); + + uri = ios.newURI("http://foo.com:5000/file.txt"); + uri = uri + .mutate() + .setPort(80) + .finalize(); + Assert.equal(uri.hostPort, "foo.com"); +} diff --git a/netwerk/test/unit/test_bug396389.js b/netwerk/test/unit/test_bug396389.js new file mode 100644 index 0000000000..5095a77bd4 --- /dev/null +++ b/netwerk/test/unit/test_bug396389.js @@ -0,0 +1,71 @@ +"use strict"; + +function round_trip(uri) { + var objectOutStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIObjectOutputStream + ); + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(false, false, 0, 0xffffffff, null); + objectOutStream.setOutputStream(pipe.outputStream); + objectOutStream.writeCompoundObject(uri, Ci.nsISupports, true); + objectOutStream.close(); + + var objectInStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIObjectInputStream + ); + objectInStream.setInputStream(pipe.inputStream); + return objectInStream.readObject(true).QueryInterface(Ci.nsIURI); +} + +var prefData = [ + { + name: "network.IDN_show_punycode", + newVal: false, + }, + { + name: "network.IDN.whitelist.ch", + newVal: true, + }, +]; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var uri1 = ios.newURI("file:///"); + Assert.ok(uri1 instanceof Ci.nsIFileURL); + + var uri2 = uri1.mutate().finalize(); + Assert.ok(uri2 instanceof Ci.nsIFileURL); + Assert.ok(uri1.equals(uri2)); + + var uri3 = round_trip(uri1); + Assert.ok(uri3 instanceof Ci.nsIFileURL); + Assert.ok(uri1.equals(uri3)); + + // Make sure our prefs are set such that this test actually means something + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + for (var pref of prefData) { + prefs.setBoolPref(pref.name, pref.newVal); + } + + try { + // URI stolen from + // http://lists.w3.org/Archives/Public/public-iri/2004Mar/0012.html + var uri4 = ios.newURI("http://xn--jos-dma.example.net.ch/"); + Assert.equal(uri4.asciiHost, "xn--jos-dma.example.net.ch"); + Assert.equal(uri4.displayHost, "jos\u00e9.example.net.ch"); + + var uri5 = round_trip(uri4); + Assert.ok(uri4.equals(uri5)); + Assert.equal(uri4.displayHost, uri5.displayHost); + Assert.equal(uri4.asciiHost, uri5.asciiHost); + } finally { + for (var pref of prefData) { + if (prefs.prefHasUserValue(pref.name)) { + prefs.clearUserPref(pref.name); + } + } + } +} diff --git a/netwerk/test/unit/test_bug401564.js b/netwerk/test/unit/test_bug401564.js new file mode 100644 index 0000000000..659fda4ab5 --- /dev/null +++ b/netwerk/test/unit/test_bug401564.js @@ -0,0 +1,44 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; +const noRedirectURI = "/content"; +const pageValue = "Final page"; +const acceptType = "application/json"; + +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Moved Temporarily"); + response.setHeader("Location", noRedirectURI, false); +} + +function contentHandler(metadata, response) { + Assert.equal(metadata.getHeader("Accept"), acceptType); + httpserver.stop(do_test_finished); +} + +function dummyHandler(request, buffer) {} + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/redirect", redirectHandler); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setBoolPref("network.http.prompt-temp-redirect", false); + + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/redirect", + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.setRequestHeader("Accept", acceptType, false); + + chan.asyncOpen(new ChannelListener(dummyHandler, null)); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug411952.js b/netwerk/test/unit/test_bug411952.js new file mode 100644 index 0000000000..a7bdfb178f --- /dev/null +++ b/netwerk/test/unit/test_bug411952.js @@ -0,0 +1,53 @@ +"use strict"; + +function run_test() { + try { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + Assert.notEqual(cm, null, "Retrieving the cookie manager failed"); + + const time = new Date("Jan 1, 2030").getTime() / 1000; + cm.add( + "example.com", + "/", + "C", + "V", + false, + true, + false, + time, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + const now = Math.floor(new Date().getTime() / 1000); + + var found = false; + for (let cookie of cm.cookies) { + if ( + cookie.host == "example.com" && + cookie.path == "/" && + cookie.name == "C" + ) { + Assert.ok( + "creationTime" in cookie, + "creationTime attribute is not accessible on the cookie" + ); + var creationTime = Math.floor(cookie.creationTime / 1000000); + // allow the times to slip by one second at most, + // which should be fine under normal circumstances. + Assert.ok( + Math.abs(creationTime - now) <= 1, + "Cookie's creationTime is set incorrectly" + ); + found = true; + break; + } + } + + Assert.ok(found, "Didn't find the cookie we were after"); + } catch (e) { + do_throw("Unexpected exception: " + e.toString()); + } + + do_test_finished(); +} diff --git a/netwerk/test/unit/test_bug412457.js b/netwerk/test/unit/test_bug412457.js new file mode 100644 index 0000000000..b5549352a8 --- /dev/null +++ b/netwerk/test/unit/test_bug412457.js @@ -0,0 +1,117 @@ +"use strict"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + // check if hostname is unescaped before applying IDNA + var newURI = ios.newURI("http://\u5341%2ecom/"); + Assert.equal(newURI.asciiHost, "xn--kkr.com"); + + // escaped UTF8 + newURI = newURI + .mutate() + .setSpec("http://%e5%8d%81.com") + .finalize(); + Assert.equal(newURI.asciiHost, "xn--kkr.com"); + + // There should be only allowed characters in hostname after + // unescaping and attempting to apply IDNA. "\x80" is illegal in + // UTF-8, so IDNA fails, and 0x80 is illegal in DNS too. + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://%80.com") + .finalize(); + }, + /NS_ERROR_UNEXPECTED/, + "illegal UTF character" + ); + + // test parsing URL with all possible host terminators + newURI = newURI + .mutate() + .setSpec("http://example.com?foo") + .finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI + .mutate() + .setSpec("http://example.com#foo") + .finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI + .mutate() + .setSpec("http://example.com:80") + .finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + newURI = newURI + .mutate() + .setSpec("http://example.com/foo") + .finalize(); + Assert.equal(newURI.asciiHost, "example.com"); + + // Characters that are invalid in the host + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%3ffoo") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%23foo") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%3bfoo") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%3a80") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%2ffoo") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + Assert.throws( + () => { + newURI = newURI + .mutate() + .setSpec("http://example.com%00") + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); +} diff --git a/netwerk/test/unit/test_bug412945.js b/netwerk/test/unit/test_bug412945.js new file mode 100644 index 0000000000..33bfda4de6 --- /dev/null +++ b/netwerk/test/unit/test_bug412945.js @@ -0,0 +1,42 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function TestListener() {} + +TestListener.prototype.onStartRequest = function(request) {}; + +TestListener.prototype.onStopRequest = function(request, status) { + httpserv.stop(do_test_finished); +}; + +function run_test() { + httpserv = new HttpServer(); + + httpserv.registerPathHandler("/bug412945", bug412945); + + httpserv.start(-1); + + // make request + var channel = NetUtil.newChannel({ + uri: "http://localhost:" + httpserv.identity.primaryPort + "/bug412945", + loadUsingSystemPrincipal: true, + }); + + channel.QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = "POST"; + channel.asyncOpen(new TestListener(), null); + + do_test_pending(); +} + +function bug412945(metadata, response) { + if ( + !metadata.hasHeader("Content-Length") || + metadata.getHeader("Content-Length") != "0" + ) { + do_throw("Content-Length header not found!"); + } +} diff --git a/netwerk/test/unit/test_bug414122.js b/netwerk/test/unit/test_bug414122.js new file mode 100644 index 0000000000..49251da7fe --- /dev/null +++ b/netwerk/test/unit/test_bug414122.js @@ -0,0 +1,62 @@ +"use strict"; + +const PR_RDONLY = 0x1; + +var etld = Cc["@mozilla.org/network/effective-tld-service;1"].getService( + Ci.nsIEffectiveTLDService +); +var idn = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); + +function run_test() { + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fis.init( + do_get_file("effective_tld_names.dat"), + PR_RDONLY, + 0o444, + Ci.nsIFileInputStream.CLOSE_ON_EOF + ); + + var lis = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + lis.init(fis, "UTF-8", 1024, 0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + var out = { value: "" }; + do { + var more = lis.readLine(out); + var line = out.value; + + line = line.replace(/^\s+/, ""); + var firstTwo = line.substring(0, 2); // a misnomer, but whatever + if (firstTwo == "" || firstTwo == "//") { + continue; + } + + var space = line.search(/[ \t]/); + line = line.substring(0, space == -1 ? line.length : space); + + if ("*." == firstTwo) { + let rest = line.substring(2); + checkPublicSuffix( + "foo.SUPER-SPECIAL-AWESOME-PREFIX." + rest, + "SUPER-SPECIAL-AWESOME-PREFIX." + rest + ); + } else if ("!" == line.charAt(0)) { + checkPublicSuffix( + line.substring(1), + line.substring(line.indexOf(".") + 1) + ); + } else { + checkPublicSuffix("SUPER-SPECIAL-AWESOME-PREFIX." + line, line); + } + } while (more); +} + +function checkPublicSuffix(host, expectedSuffix) { + expectedSuffix = idn.convertUTF8toACE(expectedSuffix).toLowerCase(); + var actualSuffix = etld.getPublicSuffixFromHost(host); + Assert.equal(actualSuffix, expectedSuffix); +} diff --git a/netwerk/test/unit/test_bug427957.js b/netwerk/test/unit/test_bug427957.js new file mode 100644 index 0000000000..f202b92300 --- /dev/null +++ b/netwerk/test/unit/test_bug427957.js @@ -0,0 +1,106 @@ +/** + * Test for Bidi restrictions on IDNs from RFC 3454 + */ + +"use strict"; + +var idnService; + +function expected_pass(inputIDN) { + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + Assert.equal(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) { + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } catch (e) {} + + Assert.notEqual(displayIDN, inputIDN); +} + +function run_test() { + // add an IDN whitelist pref + var pbi = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + pbi.setBoolPref("network.IDN.whitelist.com", true); + + idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + /* + * In any profile that specifies bidirectional character handling, all + * three of the following requirements MUST be met: + * + * 1) The characters in section 5.8 MUST be prohibited. + */ + + // 0340; COMBINING GRAVE TONE MARK + expected_fail("foo\u0340bar.com"); + // 0341; COMBINING ACUTE TONE MARK + expected_fail("foo\u0341bar.com"); + // 200E; LEFT-TO-RIGHT MARK + expected_fail("foo\u200ebar.com"); + // 200F; RIGHT-TO-LEFT MARK + // Note: this is an RTL IDN so that it doesn't fail test 2) below + expected_fail( + "\u200f\u0645\u062B\u0627\u0644.\u0622\u0632\u0645\u0627\u06CC\u0634\u06CC" + ); + // 202A; LEFT-TO-RIGHT EMBEDDING + expected_fail("foo\u202abar.com"); + // 202B; RIGHT-TO-LEFT EMBEDDING + expected_fail("foo\u202bbar.com"); + // 202C; POP DIRECTIONAL FORMATTING + expected_fail("foo\u202cbar.com"); + // 202D; LEFT-TO-RIGHT OVERRIDE + expected_fail("foo\u202dbar.com"); + // 202E; RIGHT-TO-LEFT OVERRIDE + expected_fail("foo\u202ebar.com"); + // 206A; INHIBIT SYMMETRIC SWAPPING + expected_fail("foo\u206abar.com"); + // 206B; ACTIVATE SYMMETRIC SWAPPING + expected_fail("foo\u206bbar.com"); + // 206C; INHIBIT ARABIC FORM SHAPING + expected_fail("foo\u206cbar.com"); + // 206D; ACTIVATE ARABIC FORM SHAPING + expected_fail("foo\u206dbar.com"); + // 206E; NATIONAL DIGIT SHAPES + expected_fail("foo\u206ebar.com"); + // 206F; NOMINAL DIGIT SHAPES + expected_fail("foo\u206fbar.com"); + + /* + * 2) If a string contains any RandALCat character, the string MUST NOT + * contain any LCat character. + */ + + // www.מיץpetel.com is invalid + expected_fail("www.\u05DE\u05D9\u05E5petel.com"); + // But www.מיץפטל.com is fine because the ltr and rtl characters are in + // different labels + expected_pass("www.\u05DE\u05D9\u05E5\u05E4\u05D8\u05DC.com"); + + /* + * 3) If a string contains any RandALCat character, a RandALCat + * character MUST be the first character of the string, and a + * RandALCat character MUST be the last character of the string. + */ + + // www.1מיץ.com is invalid + expected_fail("www.1\u05DE\u05D9\u05E5.com"); + // www.!מיץ.com is invalid + expected_fail("www.!\u05DE\u05D9\u05E5.com"); + // www.מיץ!.com is invalid + expected_fail("www.\u05DE\u05D9\u05E5!.com"); + + // XXX TODO: add a test for an RTL label ending with a digit. This was + // invalid in IDNA2003 but became valid in IDNA2008 + + // But www.מיץ1פטל.com is fine + expected_pass("www.\u05DE\u05D9\u05E51\u05E4\u05D8\u05DC.com"); +} diff --git a/netwerk/test/unit/test_bug429347.js b/netwerk/test/unit/test_bug429347.js new file mode 100644 index 0000000000..7fcc898e7a --- /dev/null +++ b/netwerk/test/unit/test_bug429347.js @@ -0,0 +1,75 @@ +"use strict"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var uri1 = ios.newURI("http://example.com#bar"); + var uri2 = ios.newURI("http://example.com/#bar"); + Assert.ok(uri1.equals(uri2)); + + uri1 = uri1 + .mutate() + .setSpec("http://example.com?bar") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/?bar") + .finalize(); + Assert.ok(uri1.equals(uri2)); + + // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706 + // ";" is not parsed as special anymore and thus ends up + // in the authority component (see RFC 3986) + uri1 = uri1 + .mutate() + .setSpec("http://example.com;bar") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/;bar") + .finalize(); + Assert.ok(!uri1.equals(uri2)); + + uri1 = uri1 + .mutate() + .setSpec("http://example.com#") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/#") + .finalize(); + Assert.ok(uri1.equals(uri2)); + + uri1 = uri1 + .mutate() + .setSpec("http://example.com?") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/?") + .finalize(); + Assert.ok(uri1.equals(uri2)); + + // see https://bugzilla.mozilla.org/show_bug.cgi?id=665706 + // ";" is not parsed as special anymore and thus ends up + // in the authority component (see RFC 3986) + uri1 = uri1 + .mutate() + .setSpec("http://example.com;") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/;") + .finalize(); + Assert.ok(!uri1.equals(uri2)); + + uri1 = uri1 + .mutate() + .setSpec("http://example.com") + .finalize(); + uri2 = uri2 + .mutate() + .setSpec("http://example.com/") + .finalize(); + Assert.ok(uri1.equals(uri2)); +} diff --git a/netwerk/test/unit/test_bug455311.js b/netwerk/test/unit/test_bug455311.js new file mode 100644 index 0000000000..faa5685e34 --- /dev/null +++ b/netwerk/test/unit/test_bug455311.js @@ -0,0 +1,128 @@ +"use strict"; + +function getUrlLinkFile() { + if (mozinfo.os == "win") { + return do_get_file("test_link.url"); + } + if (mozinfo.os == "linux") { + return do_get_file("test_link.desktop"); + } + do_throw("Unexpected platform"); + return null; +} + +const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + +function NotificationCallbacks(origURI, newURI) { + this._origURI = origURI; + this._newURI = newURI; +} +NotificationCallbacks.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", + ]), + getInterface(iid) { + return this.QueryInterface(iid); + }, + asyncOnChannelRedirect(oldChan, newChan, flags, callback) { + Assert.equal(oldChan.URI.spec, this._origURI.spec); + Assert.equal(oldChan.URI, this._origURI); + Assert.equal(oldChan.originalURI.spec, this._origURI.spec); + Assert.equal(oldChan.originalURI, this._origURI); + Assert.equal(newChan.originalURI.spec, this._newURI.spec); + Assert.equal(newChan.originalURI, newChan.URI); + Assert.equal(newChan.URI.spec, this._newURI.spec); + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, +}; + +function RequestObserver(origURI, newURI, nextTest) { + this._origURI = origURI; + this._newURI = newURI; + this._nextTest = nextTest; +} +RequestObserver.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsIStreamListener", + ]), + onStartRequest(req) { + var chan = req.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.URI.spec, this._origURI.spec); + Assert.equal(chan.URI, this._origURI); + Assert.equal(chan.originalURI.spec, this._origURI.spec); + Assert.equal(chan.originalURI, this._origURI); + }, + onDataAvailable(req, stream, offset, count) { + do_throw("Unexpected call to onDataAvailable"); + }, + onStopRequest(req, status) { + var chan = req.QueryInterface(Ci.nsIChannel); + try { + Assert.equal(chan.URI.spec, this._origURI.spec); + Assert.equal(chan.URI, this._origURI); + Assert.equal(chan.originalURI.spec, this._origURI.spec); + Assert.equal(chan.originalURI, this._origURI); + Assert.equal(status, Cr.NS_ERROR_ABORT); + Assert.ok(!chan.isPending()); + } catch (e) {} + this._nextTest(); + }, +}; + +function test_cancel(linkURI, newURI) { + var chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true, + }); + Assert.equal(chan.URI, linkURI); + Assert.equal(chan.originalURI, linkURI); + chan.asyncOpen(new RequestObserver(linkURI, newURI, do_test_finished)); + Assert.ok(chan.isPending()); + chan.cancel(Cr.NS_ERROR_ABORT); + Assert.ok(chan.isPending()); +} + +function test_channel(linkURI, newURI) { + const chan = NetUtil.newChannel({ + uri: linkURI, + loadUsingSystemPrincipal: true, + }); + Assert.equal(chan.URI, linkURI); + Assert.equal(chan.originalURI, linkURI); + chan.notificationCallbacks = new NotificationCallbacks(linkURI, newURI); + chan.asyncOpen( + new RequestObserver(linkURI, newURI, () => test_cancel(linkURI, newURI)) + ); + Assert.ok(chan.isPending()); +} + +function run_test() { + if (mozinfo.os != "win" && mozinfo.os != "linux") { + return; + } + + let link = getUrlLinkFile(); + let linkURI; + if (link.isSymlink()) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(link.target); + linkURI = ios.newFileURI(file); + } else { + linkURI = ios.newFileURI(link); + } + + do_test_pending(); + test_channel(linkURI, ios.newURI("http://www.mozilla.org/")); + + if (mozinfo.os != "win") { + return; + } + + link = do_get_file("test_link.lnk"); + test_channel( + ios.newFileURI(link), + ios.newURI("file:///Z:/moz-nonexistent/index.html") + ); +} diff --git a/netwerk/test/unit/test_bug464591.js b/netwerk/test/unit/test_bug464591.js new file mode 100644 index 0000000000..d2200c1eb6 --- /dev/null +++ b/netwerk/test/unit/test_bug464591.js @@ -0,0 +1,100 @@ +// 1.percent-encoded IDN that contains blacklisted character should be converted +// to punycode, not UTF-8 string +// 2.only hostname-valid percent encoded ASCII characters should be decoded +// 3.IDN convertion must not bypassed by %00 + +"use strict"; + +let reference = [ + [ + "www.example.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozilla.org", + "www.example.xn--comwww-re3c.xn--mozill-8nf.xn--comwww-rq0c.mozilla.org", + ], +]; + +let badURIs = [ + ["www.mozill%61%2f.org"], // a slash is not valid in the hostname + ["www.e%00xample.com%e2%88%95www.mozill%d0%b0.com%e2%81%84www.mozill%61.org"], +]; + +let prefData = [ + { + name: "network.enableIDN", + newVal: true, + }, + { + name: "network.IDN_show_punycode", + newVal: false, + }, + { + name: "network.IDN.whitelist.org", + newVal: true, + }, +]; + +let prefIdnBlackList = { + name: "network.IDN.extra_blocked_chars", + minimumList: "\u2215\u0430\u2044", +}; + +function stringToURL(str) { + return Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init(Ci.nsIStandardURL.URLTYPE_AUTHORITY, 80, str, "UTF-8", null) + .finalize() + .QueryInterface(Ci.nsIURL); +} + +function run_test() { + // Make sure our prefs are set such that this test actually means something + let prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + for (let pref of prefData) { + prefs.setBoolPref(pref.name, pref.newVal); + } + + prefIdnBlackList.set = false; + try { + prefIdnBlackList.oldVal = prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data; + prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data = prefIdnBlackList.minimumList; + prefIdnBlackList.set = true; + } catch (e) {} + + registerCleanupFunction(function() { + for (let pref of prefData) { + prefs.clearUserPref(pref.name); + } + if (prefIdnBlackList.set) { + prefs.getComplexValue( + prefIdnBlackList.name, + Ci.nsIPrefLocalizedString + ).data = prefIdnBlackList.oldVal; + } + }); + + for (let i = 0; i < reference.length; ++i) { + try { + let result = stringToURL("http://" + reference[i][0]).host; + equal(result, reference[i][1]); + } catch (e) { + ok(false, "Error testing " + reference[i][0]); + } + } + + for (let i = 0; i < badURIs.length; ++i) { + Assert.throws( + () => { + stringToURL("http://" + badURIs[i][0]).host; + }, + /NS_ERROR_MALFORMED_URI/, + "bad escaped character" + ); + } +} diff --git a/netwerk/test/unit/test_bug468426.js b/netwerk/test/unit/test_bug468426.js new file mode 100644 index 0000000000..f8f6b8ed62 --- /dev/null +++ b/netwerk/test/unit/test_bug468426.js @@ -0,0 +1,129 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + // Initial request. Cached variant will have no cookie + { url: "/bug468426", server: "0", expected: "0", cookie: null }, + + // Cache now contains a variant with no value for cookie. If we don't + // set cookie we expect to receive the cached variant + { url: "/bug468426", server: "1", expected: "0", cookie: null }, + + // Cache still contains a variant with no value for cookie. If we + // set a value for cookie we expect a fresh value + { url: "/bug468426", server: "2", expected: "2", cookie: "c=2" }, + + // Cache now contains a variant with cookie "c=2". If the request + // also set cookie "c=2", we expect to receive the cached variant. + { url: "/bug468426", server: "3", expected: "2", cookie: "c=2" }, + + // Cache still contains a variant with cookie "c=2". When setting + // cookie "c=4" in the request we expect a fresh value + { url: "/bug468426", server: "4", expected: "4", cookie: "c=4" }, + + // Cache now contains a variant with cookie "c=4". When setting + // cookie "c=4" in the request we expect the cached variant + { url: "/bug468426", server: "5", expected: "4", cookie: "c=4" }, + + // Cache still contains a variant with cookie "c=4". When setting + // no cookie in the request we expect a fresh value + { url: "/bug468426", server: "6", expected: "6", cookie: null }, +]; + +function setupChannel(suffix, value, cookie) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + if (cookie != null) { + httpChan.setRequestHeader("Cookie", cookie, false); + } + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel( + tests[index].url, + tests[index].server, + tests[index].cookie + ); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + // This call happens in onStopRequest from the channel. Opening a new + // channel to the same url here is no good idea! Post it instead... + do_timeout(1, triggerNextTest); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/bug468426", handler); + httpserver.start(-1); + + // Clear cache and trigger the first test + evict_cache_entries(); + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = "unset"; + try { + body = metadata.getHeader("x-request"); + } catch (e) {} + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", getDateString(-1), false); + response.setHeader("Vary", "Cookie", false); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug468594.js b/netwerk/test/unit/test_bug468594.js new file mode 100644 index 0000000000..545ae75e5f --- /dev/null +++ b/netwerk/test/unit/test_bug468594.js @@ -0,0 +1,176 @@ +// +// This script emulates the test called "Freshness" +// by Mark Nottingham, located at +// +// http://mnot.net/javascript/xmlhttprequest/cache.html +// +// The issue with Mr. Nottinghams page is that the server +// always seems to send an Expires-header in the response, +// breaking the finer details of the test. This script has +// full control of response-headers, however, and can perform +// the intended testing plus some extra stuff. +// +// Please see RFC 2616 section 13.2.1 6th paragraph for the +// definition of "explicit expiration time" being used here. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/freshness", server: "0", expected: "0" }, + { url: "/freshness", server: "1", expected: "0" }, // cached + + // RFC 2616 section 13.9 2nd paragraph says not to heuristically cache + // querystring, but we allow it to maintain web compat + { url: "/freshness?a", server: "2", expected: "2" }, + { url: "/freshness?a", server: "3", expected: "2" }, + + // explicit expiration dates in the future should be cached + { + url: "/freshness?b", + server: "4", + expected: "4", + responseheader: "Expires: " + getDateString(1), + }, + { url: "/freshness?b", server: "5", expected: "4" }, // cached due to Expires + + { + url: "/freshness?c", + server: "6", + expected: "6", + responseheader: "Cache-Control: max-age=3600", + }, + { url: "/freshness?c", server: "7", expected: "6" }, // cached due to max-age + + // explicit expiration dates in the past should NOT be cached + { + url: "/freshness?d", + server: "8", + expected: "8", + responseheader: "Expires: " + getDateString(-1), + }, + { url: "/freshness?d", server: "9", expected: "9" }, + + { + url: "/freshness?e", + server: "10", + expected: "10", + responseheader: "Cache-Control: max-age=0", + }, + { url: "/freshness?e", server: "11", expected: "11" }, + + { url: "/freshness", server: "99", expected: "0" }, // cached +]; + +function logit(i, data) { + dump( + tests[i].url + + "\t requested [" + + tests[i].server + + "]" + + " got [" + + data + + "] expected [" + + tests[i].expected + + "]" + ); + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + triggerNextTest(); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/freshness", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", getDateString(0), false); + + var header = tests[index].responseheader; + if (header == null) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + var splitHdr = header.split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug470716.js b/netwerk/test/unit/test_bug470716.js new file mode 100644 index 0000000000..a661dc902d --- /dev/null +++ b/netwerk/test/unit/test_bug470716.js @@ -0,0 +1,171 @@ +"use strict"; + +var CC = Components.Constructor; + +const StreamCopier = CC( + "@mozilla.org/network/async-stream-copier;1", + "nsIAsyncStreamCopier", + "init" +); + +const ScriptableInputStream = CC( + "@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", + "init" +); + +const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"); + +var pipe1; +var pipe2; +var copier; +var test_result; +var test_content; +var test_source_closed; +var test_sink_closed; +var test_nr; + +var copyObserver = { + onStartRequest(request) {}, + + onStopRequest(request, statusCode) { + // check status code + Assert.equal(statusCode, test_result); + + // check number of copied bytes + Assert.equal(pipe2.inputStream.available(), test_content.length); + + // check content + var scinp = new ScriptableInputStream(pipe2.inputStream); + var content = scinp.read(scinp.available()); + Assert.equal(content, test_content); + + // check closed sink + try { + pipe2.outputStream.write("closedSinkTest", 14); + Assert.ok(!test_sink_closed); + } catch (ex) { + Assert.ok(test_sink_closed); + } + + // check closed source + try { + pipe1.outputStream.write("closedSourceTest", 16); + Assert.ok(!test_source_closed); + } catch (ex) { + Assert.ok(test_source_closed); + } + + do_timeout(0, do_test); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), +}; + +function startCopier(closeSource, closeSink) { + pipe1 = new Pipe( + true /* nonBlockingInput */, + true /* nonBlockingOutput */, + 0 /* segmentSize */, + 0xffffffff /* segmentCount */, + null /* segmentAllocator */ + ); + + pipe2 = new Pipe( + true /* nonBlockingInput */, + true /* nonBlockingOutput */, + 0 /* segmentSize */, + 0xffffffff /* segmentCount */, + null /* segmentAllocator */ + ); + + copier = new StreamCopier( + pipe1.inputStream /* aSource */, + pipe2.outputStream /* aSink */, + null /* aTarget */, + true /* aSourceBuffered */, + true /* aSinkBuffered */, + 8192 /* aChunkSize */, + closeSource /* aCloseSource */, + closeSink /* aCloseSink */ + ); + + copier.asyncCopy(copyObserver, null); +} + +function do_test() { + test_nr++; + test_content = "test" + test_nr; + + switch (test_nr) { + case 1: + case 2: // close sink + case 3: // close source + case 4: // close both + // test canceling transfer + // use some undefined error code to check if it is successfully passed + // to the request observer + test_result = 0x87654321; + + test_source_closed = (test_nr - 1) >> 1 != 0; + test_sink_closed = (test_nr - 1) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + pipe1.outputStream.flush(); + do_timeout(20, function() { + copier.cancel(test_result); + pipe1.outputStream.write("a", 1); + }); + break; + case 5: + case 6: // close sink + case 7: // close source + case 8: // close both + // test copying with EOF on source + test_result = 0; + + test_source_closed = (test_nr - 5) >> 1 != 0; + test_sink_closed = (test_nr - 5) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + // we will close the source + test_source_closed = true; + pipe1.outputStream.close(); + break; + case 9: + case 10: // close sink + case 11: // close source + case 12: // close both + // test copying with error on sink + // use some undefined error code to check if it is successfully passed + // to the request observer + test_result = 0x87654321; + + test_source_closed = (test_nr - 9) >> 1 != 0; + test_sink_closed = (test_nr - 9) % 2 != 0; + + startCopier(test_source_closed, test_sink_closed); + pipe1.outputStream.write(test_content, test_content.length); + pipe1.outputStream.flush(); + // we will close the sink + test_sink_closed = true; + do_timeout(20, function() { + pipe2.outputStream + .QueryInterface(Ci.nsIAsyncOutputStream) + .closeWithStatus(test_result); + pipe1.outputStream.write("a", 1); + }); + break; + case 13: + do_test_finished(); + break; + } +} + +function run_test() { + test_nr = 0; + do_timeout(0, do_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug477578.js b/netwerk/test/unit/test_bug477578.js new file mode 100644 index 0000000000..c21b1281da --- /dev/null +++ b/netwerk/test/unit/test_bug477578.js @@ -0,0 +1,51 @@ +// test that methods are not normalized + +"use strict"; + +const testMethods = [ + ["GET"], + ["get"], + ["Get"], + ["gET"], + ["gEt"], + ["post"], + ["POST"], + ["head"], + ["HEAD"], + ["put"], + ["PUT"], + ["delete"], + ["DELETE"], + ["connect"], + ["CONNECT"], + ["options"], + ["trace"], + ["track"], + ["copy"], + ["index"], + ["lock"], + ["m-post"], + ["mkcol"], + ["move"], + ["propfind"], + ["proppatch"], + ["unlock"], + ["link"], + ["LINK"], + ["foo"], + ["foO"], + ["fOo"], + ["Foo"], +]; + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://localhost/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + for (var i = 0; i < testMethods.length; i++) { + chan.requestMethod = testMethods[i]; + Assert.equal(chan.requestMethod, testMethods[i]); + } +} diff --git a/netwerk/test/unit/test_bug479413.js b/netwerk/test/unit/test_bug479413.js new file mode 100644 index 0000000000..8a0ce0752e --- /dev/null +++ b/netwerk/test/unit/test_bug479413.js @@ -0,0 +1,61 @@ +/** + * Test for unassigned code points in IDNs (RFC 3454 section 7) + */ + +"use strict"; + +var idnService; + +function expected_pass(inputIDN) { + var isASCII = {}; + var displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + Assert.equal(displayIDN, inputIDN); +} + +function expected_fail(inputIDN) { + var isASCII = {}; + var displayIDN = ""; + + try { + displayIDN = idnService.convertToDisplayIDN(inputIDN, isASCII); + } catch (e) {} + + Assert.notEqual(displayIDN, inputIDN); +} + +function run_test() { + // add an IDN whitelist pref + var pbi = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + var whitelistPref = "network.IDN.whitelist.com"; + + pbi.setBoolPref(whitelistPref, true); + + idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + // assigned code point + expected_pass("foo\u0101bar.com"); + + // assigned code point in punycode. Should *fail* because the URL will be + // converted to Unicode for display + expected_fail("xn--foobar-5za.com"); + + // unassigned code point + expected_fail("foo\u3040bar.com"); + + // unassigned code point in punycode. Should *pass* because the URL will not + // be converted to Unicode + expected_pass("xn--foobar-533e.com"); + + // code point assigned since Unicode 3.0 + // XXX This test will unexpectedly pass when we update to IDNAbis + expected_fail("foo\u0370bar.com"); + + // reset the pref + if (pbi.prefHasUserValue(whitelistPref)) { + pbi.clearUserPref(whitelistPref); + } +} diff --git a/netwerk/test/unit/test_bug479485.js b/netwerk/test/unit/test_bug479485.js new file mode 100644 index 0000000000..bde98c6b70 --- /dev/null +++ b/netwerk/test/unit/test_bug479485.js @@ -0,0 +1,65 @@ +"use strict"; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + var test_port = function(port, exception_expected) { + dump((port || "no port provided") + "\n"); + var exception_threw = false; + try { + var newURI = ios.newURI("http://foo.com" + port); + } catch (e) { + exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (exception_threw != exception_expected) { + do_throw( + "We did" + + (exception_expected ? "n't" : "") + + " throw NS_ERROR_MALFORMED_URI when creating a new URI with " + + port + + " as a port" + ); + } + Assert.equal(exception_threw, exception_expected); + + exception_threw = false; + newURI = ios.newURI("http://foo.com"); + try { + newURI + .mutate() + .setSpec("http://foo.com" + port) + .finalize(); + } catch (e) { + exception_threw = e.result == Cr.NS_ERROR_MALFORMED_URI; + } + if (exception_threw != exception_expected) { + do_throw( + "We did" + + (exception_expected ? "n't" : "") + + " throw NS_ERROR_MALFORMED_URI when setting a spec of a URI with " + + port + + " as a port" + ); + } + Assert.equal(exception_threw, exception_expected); + }; + + test_port(":invalid", true); + test_port(":-2", true); + test_port(":-1", true); + test_port(":0", false); + test_port( + ":185891548721348172817857824356013651809236172635716571865023757816234081723451516780356", + true + ); + + // Following 3 tests are all failing, we do not throw, although we parse the whole string and use only 5870 as a portnumber + test_port(":5870:80", true); + test_port(":5870-80", true); + test_port(":5870+80", true); + + // Just a regression check + test_port(":5870", false); + test_port(":80", false); + test_port("", false); +} diff --git a/netwerk/test/unit/test_bug482601.js b/netwerk/test/unit/test_bug482601.js new file mode 100644 index 0000000000..a583a21663 --- /dev/null +++ b/netwerk/test/unit/test_bug482601.js @@ -0,0 +1,262 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv = null; +var test_nr = 0; +var observers_called = ""; +var handlers_called = ""; +var buffer = ""; + +var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + if (observers_called.length) { + observers_called += ","; + } + + observers_called += topic; + }, +}; + +var listener = { + onStartRequest(request) { + buffer = ""; + }, + + onDataAvailable(request, stream, offset, count) { + buffer = buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + Assert.equal(buffer, "0123456789"); + Assert.equal(observers_called, results[test_nr]); + test_nr++; + do_timeout(0, do_test); + }, +}; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/bug482601/nocache", bug482601_nocache); + httpserv.registerPathHandler("/bug482601/partial", bug482601_partial); + httpserv.registerPathHandler("/bug482601/cached", bug482601_cached); + httpserv.registerPathHandler( + "/bug482601/only_from_cache", + bug482601_only_from_cache + ); + httpserv.start(-1); + + var obs = Cc["@mozilla.org/observer-service;1"].getService(); + obs = obs.QueryInterface(Ci.nsIObserverService); + obs.addObserver(observer, "http-on-examine-response"); + obs.addObserver(observer, "http-on-examine-merged-response"); + obs.addObserver(observer, "http-on-examine-cached-response"); + + do_timeout(0, do_test); + do_test_pending(); +} + +function do_test() { + if (test_nr < tests.length) { + tests[test_nr](); + } else { + Assert.equal(handlers_called, "nocache,partial,cached"); + httpserv.stop(do_test_finished); + } +} + +var tests = [test_nocache, test_partial, test_cached, test_only_from_cache]; + +var results = [ + "http-on-examine-response", + "http-on-examine-response,http-on-examine-merged-response", + "http-on-examine-response,http-on-examine-merged-response", + "http-on-examine-cached-response", +]; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function storeCache(aCacheEntry, aResponseHeads, aContent) { + aCacheEntry.setMetaDataElement("request-method", "GET"); + aCacheEntry.setMetaDataElement("response-head", aResponseHeads); + aCacheEntry.setMetaDataElement("charset", "ISO-8859-1"); + + var oStream = aCacheEntry.openOutputStream(0, aContent.length); + var written = oStream.write(aContent, aContent.length); + if (written != aContent.length) { + do_throw( + "oStream.write has not written all data!\n" + + " Expected: " + + written + + "\n" + + " Actual: " + + aContent.length + + "\n" + ); + } + oStream.close(); + aCacheEntry.close(); +} + +function test_nocache() { + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/nocache" + ); + chan.asyncOpen(listener); +} + +function test_partial() { + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_partial2 + ); +} + +function test_partial2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/partial" + ); + chan.asyncOpen(listener); +} + +function test_cached() { + asyncOpenCacheEntry( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_cached2 + ); +} + +function test_cached2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123456789" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/bug482601/cached" + ); + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function test_only_from_cache() { + asyncOpenCacheEntry( + "http://localhost:" + + httpserv.identity.primaryPort + + "/bug482601/only_from_cache", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_only_from_cache2 + ); +} + +function test_only_from_cache2(status, entry) { + Assert.equal(status, Cr.NS_OK); + storeCache( + entry, + "HTTP/1.1 200 OK\r\n" + + "Date: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Server: httpd.js\r\n" + + "Last-Modified: Thu, 1 Jan 2009 00:00:00 GMT\r\n" + + "Accept-Ranges: bytes\r\n" + + "Content-Length: 10\r\n" + + "Content-Type: text/plain\r\n", + "0123456789" + ); + + observers_called = ""; + + var chan = makeChan( + "http://localhost:" + + httpserv.identity.primaryPort + + "/bug482601/only_from_cache" + ); + chan.loadFlags = Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE; + chan.asyncOpen(listener); +} + +// PATHS + +// /bug482601/nocache +function bug482601_nocache(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + var body = "0123456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called += "nocache"; +} + +// /bug482601/partial +function bug482601_partial(metadata, response) { + Assert.ok(metadata.hasHeader("If-Range")); + Assert.equal(metadata.getHeader("If-Range"), "Thu, 1 Jan 2009 00:00:00 GMT"); + Assert.ok(metadata.hasHeader("Range")); + Assert.equal(metadata.getHeader("Range"), "bytes=4-"); + + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "bytes 4-9/10", false); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", "Thu, 1 Jan 2009 00:00:00 GMT"); + + var body = "456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called += ",partial"; +} + +// /bug482601/cached +function bug482601_cached(metadata, response) { + Assert.ok(metadata.hasHeader("If-Modified-Since")); + Assert.equal( + metadata.getHeader("If-Modified-Since"), + "Thu, 1 Jan 2009 00:00:00 GMT" + ); + + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + handlers_called += ",cached"; +} + +// /bug482601/only_from_cache +function bug482601_only_from_cache(metadata, response) { + do_throw("This should not be reached"); +} diff --git a/netwerk/test/unit/test_bug482934.js b/netwerk/test/unit/test_bug482934.js new file mode 100644 index 0000000000..2827e4c875 --- /dev/null +++ b/netwerk/test/unit/test_bug482934.js @@ -0,0 +1,189 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var response_code; +var response_body; + +var request_time; +var response_time; + +var cache_storage; + +var httpserver = new HttpServer(); +httpserver.start(-1); + +var base_url = "http://localhost:" + httpserver.identity.primaryPort; +var resource = "/resource"; +var resource_url = base_url + resource; + +// Test flags +var hit_server = false; + +function make_channel(aUrl) { + // Reset test global status + hit_server = false; + + var req = NetUtil.newChannel({ uri: aUrl, loadUsingSystemPrincipal: true }); + req.QueryInterface(Ci.nsIHttpChannel); + req.setRequestHeader("If-Modified-Since", request_time, false); + return req; +} + +function make_uri(aUrl) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(aUrl); +} + +function resource_handler(aMetadata, aResponse) { + hit_server = true; + Assert.ok(aMetadata.hasHeader("If-Modified-Since")); + Assert.equal(aMetadata.getHeader("If-Modified-Since"), request_time); + + if (response_code == "200") { + aResponse.setStatusLine(aMetadata.httpVersion, 200, "OK"); + aResponse.setHeader("Content-Type", "text/plain", false); + aResponse.setHeader("Last-Modified", response_time, false); + + aResponse.bodyOutputStream.write(response_body, response_body.length); + } else if (response_code == "304") { + aResponse.setStatusLine(aMetadata.httpVersion, 304, "Not Modified"); + aResponse.setHeader("Returned-From-Handler", "1"); + } +} + +function check_cached_data(aCachedData, aCallback) { + asyncOpenCacheEntry( + resource_url, + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(aStatus, aEntry) { + Assert.equal(aStatus, Cr.NS_OK); + pumpReadStream(aEntry.openInputStream(0), function(aData) { + Assert.equal(aData, aCachedData); + aCallback(); + }); + } + ); +} + +function run_test() { + do_get_profile(); + evict_cache_entries(); + + do_test_pending(); + + cache_storage = getCacheStorage("disk"); + httpserver.registerPathHandler(resource, resource_handler); + + wait_for_cache_index(run_next_test); +} + +// 1. send custom conditional request when we don't have an entry +// server returns 304 -> client receives 304 +add_test(() => { + response_code = "304"; + response_body = ""; + request_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + response_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function(aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 304 + ); + Assert.ok(!cache_storage.exists(make_uri(resource_url), "")); + Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1"); + + run_next_test(); + }, true); + }, null) + ); +}); + +// 2. send custom conditional request when we don't have an entry +// server returns 200 -> result is cached +add_test(() => { + response_code = "200"; + response_body = "content_body"; + request_time = "Thu, 1 Jan 2009 00:00:00 GMT"; + response_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function(aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 200 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + + check_cached_data(response_body, run_next_test); + }, true); + }, null) + ); +}); + +// 3. send custom conditional request when we have an entry +// server returns 304 -> client receives 304 and cached entry is unchanged +add_test(() => { + response_code = "304"; + var cached_body = response_body; + response_body = ""; + request_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + response_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function(aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 304 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + Assert.equal(aRequest.getResponseHeader("Returned-From-Handler"), "1"); + Assert.equal(aData, ""); + + // Check the cache data is not changed + check_cached_data(cached_body, run_next_test); + }, true); + }, null) + ); +}); + +// 4. send custom conditional request when we have an entry +// server returns 200 -> result is cached +add_test(() => { + response_code = "200"; + response_body = "updated_content_body"; + request_time = "Fri, 2 Jan 2009 00:00:00 GMT"; + response_time = "Sat, 3 Jan 2009 00:00:00 GMT"; + var ch = make_channel(resource_url); + ch.asyncOpen( + new ChannelListener(function(aRequest, aData) { + syncWithCacheIOThread(() => { + Assert.ok(hit_server); + Assert.equal( + aRequest.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 200 + ); + Assert.ok(cache_storage.exists(make_uri(resource_url), "")); + + // Check the cache data is updated + check_cached_data(response_body, () => { + run_next_test(); + httpserver.stop(do_test_finished); + }); + }, true); + }, null) + ); +}); diff --git a/netwerk/test/unit/test_bug484684.js b/netwerk/test/unit/test_bug484684.js new file mode 100644 index 0000000000..49a7f79205 --- /dev/null +++ b/netwerk/test/unit/test_bug484684.js @@ -0,0 +1,139 @@ +"use strict"; + +const URL = "ftp://localhost/bug464884/"; + +const tests = [ + // standard ls unix format + [ + "-rw-rw-r-- 1 500 500 0 Jan 01 2000 file1\r\n" + + "-rw-rw-r-- 1 500 500 0 Jan 01 2000 file2\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "file1" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' + + '201: "%20file2" 0 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n', + ], + // old Hellsoft unix format + [ + "-[RWCEMFA] supervisor 214059 Jan 01 2000 file1\r\n" + + "-[RWCEMFA] supervisor 214059 Jan 01 2000 file2\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "file1" 214059 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' + + '201: "file2" 214059 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n', + ], + // new Hellsoft unix format + [ + "- [RWCEAFMS] jrd 192 Jan 01 2000 file1\r\n" + + "- [RWCEAFMS] jrd 192 Jan 01 2000 file2\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "file1" 192 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n' + + '201: "%20file2" 192 Sat%2C%2001%20Jan%202000%2000%3A00%3A00%20GMT FILE \n', + ], + // DOS format with correct offsets + [ + "01-01-00 01:00AM dir1\r\n" + + "01-01-00 01:00AM junction1 -> foo1\r\n" + + "01-01-00 01:00AM 95077 file1\r\n" + + "01-01-00 01:00AM dir2\r\n" + + "01-01-00 01:00AM junction2 -> foo2\r\n" + + "01-01-00 01:00AM 95077 file2\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "dir1" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' + + '201: "junction1" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' + + '201: "file1" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n' + + '201: "%20dir2" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' + + '201: "%20junction2" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' + + '201: "%20file2" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n', + ], + // DOS format with wrong offsets + [ + "01-01-00 01:00AM dir1\r\n" + + "01-01-00 01:00AM dir2\r\n" + + "01-01-00 01:00AM dir3\r\n" + + "01-01-00 01:00AM junction1 -> foo1\r\n" + + "01-01-00 01:00AM junction2 -> foo2\r\n" + + "01-01-00 01:00AM junction3 -> foo3\r\n" + + "01-01-00 01:00AM 95077 file1\r\n" + + "01-01-00 01:00AM 95077 file2\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "dir1" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' + + '201: "dir2" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' + + '201: "dir3" 0 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT DIRECTORY \n' + + '201: "junction1" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' + + '201: "junction2" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' + + '201: "junction3" Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT SYMBOLIC-LINK \n' + + '201: "file1" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n' + + '201: "file2" 95077 Sat%2C%2001%20Jan%202000%2001%3A00%3A00%20GMT FILE \n', + ], +]; + +function checkData(request, data, ctx) { + Assert.equal(tests[0][1], data); + tests.shift(); + executeSoon(next_test); +} + +function storeData(status, entry) { + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData( + "text/ftp-dir", + "application/http-index-format", + new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL), + null + ); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = tests[0][0]; + + var url = NetUtil.newURI(URL); + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending() { + return this.pending; + }, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, stream, 0, 0); + channel.pending = false; + converter.onStopRequest(channel, null, Cr.NS_OK); +} + +function next_test() { + if (tests.length == 0) { + do_test_finished(); + } else { + storeData(); + } +} + +function run_test() { + executeSoon(next_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug490095.js b/netwerk/test/unit/test_bug490095.js new file mode 100644 index 0000000000..7e8de69af1 --- /dev/null +++ b/netwerk/test/unit/test_bug490095.js @@ -0,0 +1,158 @@ +// +// Verify that the VALIDATE_NEVER and LOAD_FROM_CACHE flags override +// heuristic query freshness as defined in RFC 2616 section 13.9 +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/freshness?a", server: "0", expected: "0" }, + { url: "/freshness?a", server: "1", expected: "1" }, + + // Setting the VALIDATE_NEVER flag should grab entry from cache + { + url: "/freshness?a", + server: "2", + expected: "1", + flags: Ci.nsIRequest.VALIDATE_NEVER, + }, + + // Finally, check that request is validated with no flags set + { url: "/freshness?a", server: "99", expected: "99" }, + + { url: "/freshness?b", server: "0", expected: "0" }, + { url: "/freshness?b", server: "1", expected: "1" }, + + // Setting the LOAD_FROM_CACHE flag also grab the entry from cache + { + url: "/freshness?b", + server: "2", + expected: "1", + flags: Ci.nsIRequest.LOAD_FROM_CACHE, + }, + + // Finally, check that request is validated with no flags set + { url: "/freshness?b", server: "99", expected: "99" }, +]; + +function logit(i, data) { + dump( + tests[i].url + + "\t requested [" + + tests[i].server + + "]" + + " got [" + + data + + "] expected [" + + tests[i].expected + + "]" + ); + if (tests[i].responseheader) { + dump("\t[" + tests[i].responseheader + "]"); + } + dump("\n"); +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + return httpChan; +} + +function triggerNextTest() { + var test = tests[index]; + var channel = setupChannel(test.url, test.server); + if (test.flags) { + channel.loadFlags = test.flags; + } + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + logit(index, data); + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + // this call happens in onStopRequest from the channel, and opening a + // new channel to the same url here is no good idea... post it instead + do_timeout(1, triggerNextTest); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/freshness", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + var body = metadata.getHeader("x-request"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", getDateString(0), false); + response.setHeader("Cache-Control", "max-age=0", false); + + var header = tests[index].responseheader; + if (header == null) { + response.setHeader("Last-Modified", getDateString(-1), false); + } else { + var splitHdr = header.split(": "); + response.setHeader(splitHdr[0], splitHdr[1], false); + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug504014.js b/netwerk/test/unit/test_bug504014.js new file mode 100644 index 0000000000..e4fe449648 --- /dev/null +++ b/netwerk/test/unit/test_bug504014.js @@ -0,0 +1,74 @@ +"use strict"; + +var valid_URIs = [ + "http://[::]/", + "http://[::1]/", + "http://[1::]/", + "http://[::]/", + "http://[::1]/", + "http://[1::]/", + "http://[1:2:3:4:5:6:7::]/", + "http://[::1:2:3:4:5:6:7]/", + "http://[1:2:a:B:c:D:e:F]/", + "http://[1::8]/", + "http://[1:2::8]/", + "http://[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]/", + "http://[::192.168.1.1]/", + "http://[1::0.0.0.0]/", + "http://[1:2::255.255.255.255]/", + "http://[1:2:3::255.255.255.255]/", + "http://[1:2:3:4::255.255.255.255]/", + "http://[1:2:3:4:5::255.255.255.255]/", + "http://[1:2:3:4:5:6:255.255.255.255]/", +]; + +var invalid_URIs = [ + "http://[1]/", + "http://[192.168.1.1]/", + "http://[:::]/", + "http://[:::1]/", + "http://[1:::]/", + "http://[::1::]/", + "http://[1:2:3:4:5:6:7:]/", + "http://[:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7:8:]/", + "http://[:1:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7:8::]/", + "http://[::1:2:3:4:5:6:7:8]/", + "http://[1:2:3:4:5:6:7]/", + "http://[1:2:3:4:5:6:7:8:9]/", + "http://[00001:2:3:4:5:6:7:8]/", + "http://[0001:2:3:4:5:6:7:89abc]/", + "http://[A:b:C:d:E:f:G:h]/", + "http://[::192.168.1]/", + "http://[::192.168.1.]/", + "http://[::.168.1.1]/", + "http://[::192..1.1]/", + "http://[::0192.168.1.1]/", + "http://[::256.255.255.255]/", + "http://[::1x.255.255.255]/", + "http://[::192.4294967464.1.1]/", + "http://[1:2:3:4:5:6::255.255.255.255]/", + "http://[1:2:3:4:5:6:7:255.255.255.255]/", +]; + +function run_test() { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + for (var i = 0; i < valid_URIs.length; i++) { + try { + ios.newURI(valid_URIs[i]); + } catch (e) { + do_throw("cannot create URI:" + valid_URIs[i]); + } + } + + for (var i = 0; i < invalid_URIs.length; i++) { + try { + ios.newURI(invalid_URIs[i]); + do_throw("should throw: " + invalid_URIs[i]); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI); + } + } +} diff --git a/netwerk/test/unit/test_bug510359.js b/netwerk/test/unit/test_bug510359.js new file mode 100644 index 0000000000..321da39912 --- /dev/null +++ b/netwerk/test/unit/test_bug510359.js @@ -0,0 +1,102 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { url: "/bug510359", server: "0", expected: "0" }, + { url: "/bug510359", server: "1", expected: "1" }, +]; + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + httpChan.setRequestHeader("x-request", value, false); + httpChan.setRequestHeader("Cookie", "c=" + value, false); + return httpChan; +} + +function triggerNextTest() { + var channel = setupChannel(tests[index].url, tests[index].server); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal(tests[index].expected, data); + + if (index < tests.length - 1) { + index++; + triggerNextTest(); + } else { + httpserver.stop(do_test_finished); + } +} + +function run_test() { + httpserver.registerPathHandler("/bug510359", handler); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + triggerNextTest(); + + do_test_pending(); +} + +function handler(metadata, response) { + try { + metadata.getHeader("If-Modified-Since"); + response.setStatusLine(metadata.httpVersion, 500, "Failed"); + var msg = "Client should not set If-Modified-Since header"; + response.bodyOutputStream.write(msg, msg.length); + } catch (ex) { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", getDateString(-1), false); + response.setHeader("Vary", "Cookie", false); + var body = metadata.getHeader("x-request"); + response.bodyOutputStream.write(body, body.length); + } +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug515583.js b/netwerk/test/unit/test_bug515583.js new file mode 100644 index 0000000000..fe2bad5399 --- /dev/null +++ b/netwerk/test/unit/test_bug515583.js @@ -0,0 +1,82 @@ +"use strict"; + +const URL = "ftp://localhost/bug515583/"; + +const tests = [ + [ + "[RWCEM1 4 1-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1] 4 2-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1]A 4 3-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1]B; 4 4-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1];1 4 5-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1]; 4 6-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1]C;1D 4 7-MAR-1993 18:09:01.12\r\n" + + "[RWCEM1]E;1 4 8-MAR-1993 18:09:01.12\r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "A" 2048 Wed%2C%2003%20Mar%201993%2018%3A09%3A01%20GMT FILE \n' + + '201: "E" 2048 Mon%2C%2008%20Mar%201993%2018%3A09%3A01%20GMT FILE \n', + ], + [ + "\r\r\r\n", + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n", + ], +]; + +function checkData(request, data, ctx) { + Assert.equal(tests[0][1], data); + tests.shift(); + executeSoon(next_test); +} + +function storeData(status, entry) { + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData( + "text/ftp-dir", + "application/http-index-format", + new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL), + null + ); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = tests[0][0]; + + var url = NetUtil.newURI(URL); + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending() { + return this.pending; + }, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, stream, 0, 0); + channel.pending = false; + converter.onStopRequest(channel, null, Cr.NS_OK); +} + +function next_test() { + if (tests.length == 0) { + do_test_finished(); + } else { + storeData(); + } +} + +function run_test() { + executeSoon(next_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug526789.js b/netwerk/test/unit/test_bug526789.js new file mode 100644 index 0000000000..07ce2d18a3 --- /dev/null +++ b/netwerk/test/unit/test_bug526789.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + var expiry = (Date.now() + 1000) * 1000; + + cm.removeAll(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + // test that variants of 'baz.com' get normalized appropriately, but that + // malformed hosts are rejected + cm.add( + "baz.com", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("baz.com"), 1); + Assert.equal(cm.countCookiesFromHost("BAZ.com"), 1); + Assert.equal(cm.countCookiesFromHost(".baz.com"), 1); + Assert.equal(cm.countCookiesFromHost("baz.com."), 0); + Assert.equal(cm.countCookiesFromHost(".baz.com."), 0); + do_check_throws(function() { + cm.countCookiesFromHost("baz.com.."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost("baz..com"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost("..baz.com"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + cm.remove("BAZ.com.", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com"), 1); + cm.remove("baz.com", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com"), 0); + + // Test that 'baz.com' and 'baz.com.' are treated differently + cm.add( + "baz.com.", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("baz.com"), 0); + Assert.equal(cm.countCookiesFromHost("BAZ.com"), 0); + Assert.equal(cm.countCookiesFromHost(".baz.com"), 0); + Assert.equal(cm.countCookiesFromHost("baz.com."), 1); + Assert.equal(cm.countCookiesFromHost(".baz.com."), 1); + cm.remove("baz.com", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com."), 1); + cm.remove("baz.com.", "foo", "/", {}); + Assert.equal(cm.countCookiesFromHost("baz.com."), 0); + + // test that domain cookies are illegal for IP addresses, aliases such as + // 'localhost', and eTLD's such as 'co.uk' + cm.add( + "192.168.0.1", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("192.168.0.1"), 1); + Assert.equal(cm.countCookiesFromHost("192.168.0.1."), 0); + do_check_throws(function() { + cm.countCookiesFromHost(".192.168.0.1"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost(".192.168.0.1."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.add( + "localhost", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("localhost"), 1); + Assert.equal(cm.countCookiesFromHost("localhost."), 0); + do_check_throws(function() { + cm.countCookiesFromHost(".localhost"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost(".localhost."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.add( + "co.uk", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(cm.countCookiesFromHost("co.uk"), 1); + Assert.equal(cm.countCookiesFromHost("co.uk."), 0); + do_check_throws(function() { + cm.countCookiesFromHost(".co.uk"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost(".co.uk."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.removeAll(); + + CookieXPCShellUtils.createServer({ + hosts: ["baz.com", "192.168.0.1", "localhost", "co.uk", "foo.com"], + }); + + var uri = NetUtil.newURI("http://baz.com/"); + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + Assert.equal(uri.asciiHost, "baz.com"); + + await CookieXPCShellUtils.setCookieToDocument(uri.spec, "foo=bar"); + const docCookies = await CookieXPCShellUtils.getCookieStringFromDocument( + uri.spec + ); + Assert.equal(docCookies, "foo=bar"); + + Assert.equal(cm.countCookiesFromHost(""), 0); + do_check_throws(function() { + cm.countCookiesFromHost("."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.countCookiesFromHost(".."); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + var cookies = cm.getCookiesFromHost("", {}); + Assert.ok(!cookies.length); + do_check_throws(function() { + cm.getCookiesFromHost(".", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.getCookiesFromHost("..", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cookies = cm.getCookiesFromHost("baz.com", {}); + Assert.equal(cookies.length, 1); + Assert.equal(cookies[0].name, "foo"); + cookies = cm.getCookiesFromHost("", {}); + Assert.ok(!cookies.length); + do_check_throws(function() { + cm.getCookiesFromHost(".", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + cm.getCookiesFromHost("..", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + cm.removeAll(); + + // test that an empty host to add() or remove() works, + // but a host of '.' doesn't + cm.add( + "", + "/", + "foo2", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(getCookieCount(), 1); + do_check_throws(function() { + cm.add( + ".", + "/", + "foo3", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + Assert.equal(getCookieCount(), 1); + + cm.remove("", "foo2", "/", {}); + Assert.equal(getCookieCount(), 0); + do_check_throws(function() { + cm.remove(".", "foo3", "/", {}); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + // test that the 'domain' attribute accepts a leading dot for IP addresses, + // aliases such as 'localhost', and eTLD's such as 'co.uk'; but that the + // resulting cookie is for the exact host only. + await testDomainCookie("http://192.168.0.1/", "192.168.0.1"); + await testDomainCookie("http://localhost/", "localhost"); + await testDomainCookie("http://co.uk/", "co.uk"); + + // Test that trailing dots are treated differently for purposes of the + // 'domain' attribute when using setCookieStringFromDocument. + await testTrailingDotCookie("http://localhost/", "localhost"); + await testTrailingDotCookie("http://foo.com/", "foo.com"); + + cm.removeAll(); +}); + +function getCookieCount() { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + return cm.cookies.length; +} + +async function testDomainCookie(uriString, domain) { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=" + domain + ); + + var cookies = cm.getCookiesFromHost(domain, {}); + Assert.ok(cookies.length); + Assert.equal(cookies[0].host, domain); + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=." + domain + ); + + cookies = cm.getCookiesFromHost(domain, {}); + Assert.ok(cookies.length); + Assert.equal(cookies[0].host, domain); + cm.removeAll(); +} + +async function testTrailingDotCookie(uriString, domain) { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + + cm.removeAll(); + + await CookieXPCShellUtils.setCookieToDocument( + uriString, + "foo=bar; domain=" + domain + "/" + ); + + Assert.equal(cm.countCookiesFromHost(domain), 0); + Assert.equal(cm.countCookiesFromHost(domain + "."), 0); + cm.removeAll(); +} diff --git a/netwerk/test/unit/test_bug528292.js b/netwerk/test/unit/test_bug528292.js new file mode 100644 index 0000000000..dada3ef6cf --- /dev/null +++ b/netwerk/test/unit/test_bug528292.js @@ -0,0 +1,93 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const sentCookieVal = "foo=bar"; +const responseBody = "response body"; + +XPCOMUtils.defineLazyGetter(this, "baseURL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +const preRedirectPath = "/528292/pre-redirect"; + +XPCOMUtils.defineLazyGetter(this, "preRedirectURL", function() { + return baseURL + preRedirectPath; +}); + +const postRedirectPath = "/528292/post-redirect"; + +XPCOMUtils.defineLazyGetter(this, "postRedirectURL", function() { + return baseURL + postRedirectPath; +}); + +var httpServer = null; +var receivedCookieVal = null; + +function preRedirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", postRedirectURL, false); +} + +function postRedirectHandler(metadata, response) { + receivedCookieVal = metadata.getHeader("Cookie"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +add_task(async () => { + // Start the HTTP server. + httpServer = new HttpServer(); + httpServer.registerPathHandler(preRedirectPath, preRedirectHandler); + httpServer.registerPathHandler(postRedirectPath, postRedirectHandler); + httpServer.start(-1); + + if (!inChildProcess()) { + // Disable third-party cookies in general. + Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch) + .setIntPref("network.cookie.cookieBehavior", 1); + Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch) + .setBoolPref("network.cookieJarSettings.unblocked_for_testing", true); + } + + var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + + // Set up a channel with forceAllowThirdPartyCookie set to true. We'll use + // the channel both to set a cookie and then to load the pre-redirect URI. + var chan = NetUtil.newChannel({ + uri: preRedirectURL, + loadUsingSystemPrincipal: true, + }) + .QueryInterface(Ci.nsIHttpChannel) + .QueryInterface(Ci.nsIHttpChannelInternal); + chan.forceAllowThirdPartyCookie = true; + + // Set a cookie on one of the URIs. It doesn't matter which one, since + // they're both from the same host, which is enough for the cookie service + // to send the cookie with both requests. + var postRedirectURI = ioService.newURI(postRedirectURL); + + await CookieXPCShellUtils.setCookieToDocument( + postRedirectURI.spec, + sentCookieVal + ); + + // Load the pre-redirect URI. + await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve, null)); + }); + + Assert.equal(receivedCookieVal, sentCookieVal); + httpServer.stop(do_test_finished); +}); diff --git a/netwerk/test/unit/test_bug536324_64bit_content_length.js b/netwerk/test/unit/test_bug536324_64bit_content_length.js new file mode 100644 index 0000000000..9f60d7ac00 --- /dev/null +++ b/netwerk/test/unit/test_bug536324_64bit_content_length.js @@ -0,0 +1,64 @@ +/* Test to ensure our 64-bit content length implementation works, at least for + a simple HTTP case */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// This C-L is significantly larger than (U)INT32_MAX, to make sure we do +// 64-bit properly. +const CONTENT_LENGTH = "1152921504606846975"; + +var httpServer = null; + +var listener = { + onStartRequest(req) {}, + + onDataAvailable(req, stream, off, count) { + Assert.equal(req.getResponseHeader("Content-Length"), CONTENT_LENGTH); + + // We're done here, cancel the channel + req.cancel(Cr.NS_BINDING_ABORTED); + }, + + onStopRequest(req, stat) { + httpServer.stop(do_test_finished); + }, +}; + +function hugeContentLength(metadata, response) { + var text = "abcdefghijklmnopqrstuvwxyz"; + var bytes_written = 0; + + response.seizePower(); + + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Length: " + CONTENT_LENGTH + "\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + + // Write enough data to ensure onDataAvailable gets called + while (bytes_written < 4096) { + response.write(text); + bytes_written += text.length; + } + + response.finish(); +} + +function test_hugeContentLength() { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpServer.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); +} + +add_test(test_hugeContentLength); + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/", hugeContentLength); + httpServer.start(-1); + run_next_test(); +} diff --git a/netwerk/test/unit/test_bug540566.js b/netwerk/test/unit/test_bug540566.js new file mode 100644 index 0000000000..24260421ad --- /dev/null +++ b/netwerk/test/unit/test_bug540566.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +function continue_test(status, entry) { + Assert.equal(status, Cr.NS_OK); + // TODO - mayhemer: remove this tests completely + // entry.deviceID; + // if the above line does not crash, the test was successful + do_test_finished(); +} + +function run_test() { + asyncOpenCacheEntry( + "http://some.key/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + continue_test + ); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug543805.js b/netwerk/test/unit/test_bug543805.js new file mode 100644 index 0000000000..4546b81489 --- /dev/null +++ b/netwerk/test/unit/test_bug543805.js @@ -0,0 +1,136 @@ +"use strict"; + +const URL = "ftp://localhost/bug543805/"; + +var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +var year = new Date().getFullYear().toString(); +var day = dayNames[new Date(year, 0, 1).getDay()]; + +const tests = [ + // AIX ls format + [ + "-rw-r--r-- 1 0 11 Jan 1 20:19 nodup.file\r\n" + + "-rw-r--r-- 1 0 22 Jan 1 20:19 test.blankfile\r\n" + + "-rw-r--r-- 1 0 33 Apr 1 2008 test2.blankfile\r\n" + + "-rw-r--r-- 1 0 44 Jan 1 20:19 nodup.file\r\n" + + "-rw-r--r-- 1 0 55 Jan 1 20:19 test.file\r\n" + + "-rw-r--r-- 1 0 66 Apr 1 2008 test2.file\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "%20nodup.file" 11 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "%20test.blankfile" 22 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "%20test2.blankfile" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n' + + '201: "nodup.file" 44 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "test.file" 55 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "test2.file" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n', + ], + + // standard ls format + [ + "-rw-r--r-- 1 500 500 11 Jan 1 20:19 nodup.file\r\n" + + "-rw-r--r-- 1 500 500 22 Jan 1 20:19 test.blankfile\r\n" + + "-rw-r--r-- 1 500 500 33 Apr 1 2008 test2.blankfile\r\n" + + "-rw-r--r-- 1 500 500 44 Jan 1 20:19 nodup.file\r\n" + + "-rw-r--r-- 1 500 500 55 Jan 1 20:19 test.file\r\n" + + "-rw-r--r-- 1 500 500 66 Apr 1 2008 test2.file\r\n", + + "300: " + + URL + + "\n" + + "200: filename content-length last-modified file-type\n" + + '201: "%20nodup.file" 11 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "%20test.blankfile" 22 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "%20test2.blankfile" 33 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n' + + '201: "nodup.file" 44 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "test.file" 55 ' + + day + + "%2C%2001%20Jan%20" + + year + + "%2020%3A19%3A00%20GMT FILE \n" + + '201: "test2.file" 66 Tue%2C%2001%20Apr%202008%2000%3A00%3A00%20GMT FILE \n', + ], +]; + +function checkData(request, data, ctx) { + Assert.equal(tests[0][1], data); + tests.shift(); + executeSoon(next_test); +} + +function storeData(status, entry) { + var scs = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ); + var converter = scs.asyncConvertData( + "text/ftp-dir", + "application/http-index-format", + new ChannelListener(checkData, null, CL_ALLOW_UNKNOWN_CL), + null + ); + + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = tests[0][0]; + + var url = NetUtil.newURI(URL); + + var channel = { + URI: url, + contentLength: -1, + pending: true, + isPending() { + return this.pending; + }, + QueryInterface: ChromeUtils.generateQI(["nsIChannel"]), + }; + + converter.onStartRequest(channel, null); + converter.onDataAvailable(channel, stream, 0, 0); + channel.pending = false; + converter.onStopRequest(channel, null, Cr.NS_OK); +} + +function next_test() { + if (tests.length == 0) { + do_test_finished(); + } else { + storeData(); + } +} + +function run_test() { + executeSoon(next_test); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug553970.js b/netwerk/test/unit/test_bug553970.js new file mode 100644 index 0000000000..95fcf6e9bc --- /dev/null +++ b/netwerk/test/unit/test_bug553970.js @@ -0,0 +1,53 @@ +"use strict"; + +function makeURL(spec) { + return Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(spec) + .QueryInterface(Ci.nsIURL); +} + +// Checks that nsIURL::GetRelativeSpec does what it claims to do. +function run_test() { + // Elements of tests have the form [this.spec, aURIToCompare.spec, expectedResult]. + let tests = [ + [ + "http://mozilla.org/", + "http://www.mozilla.org/", + "http://www.mozilla.org/", + ], + [ + "http://mozilla.org/", + "http://www.mozilla.org", + "http://www.mozilla.org/", + ], + ["http://foo.com/bar/", "http://foo.com:80/bar/", ""], + ["http://foo.com/", "http://foo.com/a.htm#b", "a.htm#b"], + ["http://foo.com/a/b/", "http://foo.com/c", "../../c"], + ["http://foo.com/a?b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a#b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a;p?b/c/", "http://foo.com/c", "c"], + ["http://foo.com/a/b?c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b#c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b;p?c/d/", "http://foo.com/c", "../c"], + ["http://foo.com/a/b/c?d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a/b/c#d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f", "../../f"], + ["http://foo.com/a?b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a#b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a;p?b/c/", "http://foo.com/c/d", "c/d"], + ["http://foo.com/a/b?c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b#c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b;p?c/d/", "http://foo.com/c/d", "../c/d"], + ["http://foo.com/a/b/c?d/e/", "http://foo.com/f/g/", "../../f/g/"], + ["http://foo.com/a/b/c#d/e/", "http://foo.com/f/g/", "../../f/g/"], + ["http://foo.com/a/b/c;p?d/e/", "http://foo.com/f/g/", "../../f/g/"], + ]; + + for (var i = 0; i < tests.length; i++) { + let url1 = makeURL(tests[i][0]); + let url2 = makeURL(tests[i][1]); + let expected = tests[i][2]; + Assert.equal(expected, url1.getRelativeSpec(url2)); + } +} diff --git a/netwerk/test/unit/test_bug561042.js b/netwerk/test/unit/test_bug561042.js new file mode 100644 index 0000000000..cc78a40993 --- /dev/null +++ b/netwerk/test/unit/test_bug561042.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const SERVER_PORT = 8080; +const baseURL = "http://localhost:" + SERVER_PORT + "/"; + +var cookie = ""; +for (let i = 0; i < 10000; i++) { + cookie += " big cookie"; +} + +var listener = { + onStartRequest(request) {}, + + onDataAvailable(request, stream) {}, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + server.stop(do_test_finished); + }, +}; + +var server = new HttpServer(); +function run_test() { + server.start(SERVER_PORT); + server.registerPathHandler("/", function(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Set-Cookie", "BigCookie=" + cookie, false); + response.write("Hello world"); + }); + var chan = NetUtil.newChannel({ + uri: baseURL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug561276.js b/netwerk/test/unit/test_bug561276.js new file mode 100644 index 0000000000..03ff7b3e35 --- /dev/null +++ b/netwerk/test/unit/test_bug561276.js @@ -0,0 +1,64 @@ +// +// Verify that we hit the net if we discover a cycle of redirects +// coming from cache. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var iteration = 0; + +function setupChannel(suffix) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.requestMethod = "GET"; + return httpChan; +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal("Ok", data); + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver.registerPathHandler("/redirect1", redirectHandler1); + httpserver.registerPathHandler("/redirect2", redirectHandler2); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + // load first time + var channel = setupChannel("/redirect1"); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); + + do_test_pending(); +} + +function redirectHandler1(metadata, response) { + // first time we return a cacheable 302 pointing to next redirect + if (iteration < 1) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect2", false); + + // next time called we return 200 + } else { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); + } + iteration += 1; +} + +function redirectHandler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect1", false); +} diff --git a/netwerk/test/unit/test_bug580508.js b/netwerk/test/unit/test_bug580508.js new file mode 100644 index 0000000000..8cc8a7b341 --- /dev/null +++ b/netwerk/test/unit/test_bug580508.js @@ -0,0 +1,33 @@ +"use strict"; + +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); +var resProt = ioService + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + +function run_test() { + // Define a resource:// alias that points to another resource:// URI. + let greModulesURI = ioService.newURI("resource://gre/modules/"); + resProt.setSubstitution("my-gre-modules", greModulesURI); + + // When we ask for the alias, we should not get the resource:// + // URI that we registered it for but the original file URI. + let greFileSpec = ioService.newURI( + "modules/", + null, + resProt.getSubstitution("gre") + ).spec; + let aliasURI = resProt.getSubstitution("my-gre-modules"); + Assert.equal(aliasURI.spec, greFileSpec); + + // Resolving URIs using the original resource path and the alias + // should yield the same result. + let greNetUtilURI = ioService.newURI("resource://gre/modules/NetUtil.jsm"); + let myNetUtilURI = ioService.newURI("resource://my-gre-modules/NetUtil.jsm"); + Assert.equal( + resProt.resolveURI(greNetUtilURI), + resProt.resolveURI(myNetUtilURI) + ); +} diff --git a/netwerk/test/unit/test_bug586908.js b/netwerk/test/unit/test_bug586908.js new file mode 100644 index 0000000000..a6b4b8a802 --- /dev/null +++ b/netwerk/test/unit/test_bug586908.js @@ -0,0 +1,104 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); + +var httpserv = null; + +const CID = Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"); +XPCOMUtils.defineLazyGetter(this, "systemSettings", function() { + return { + QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), + + mainThreadOnly: true, + PACURI: "http://localhost:" + httpserv.identity.primaryPort + "/redirect", + getProxyForURI(aURI) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + }; +}); + +function checkValue(request, data, ctx) { + Assert.ok(called); + Assert.equal("ok", data); + httpserv.stop(do_test_finished); +} + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/redirect", redirect); + httpserv.registerPathHandler("/pac", pac); + httpserv.registerPathHandler("/target", target); + httpserv.start(-1); + + MockRegistrar.register( + "@mozilla.org/system-proxy-settings;1", + systemSettings + ); + + // Ensure we're using system-properties + const prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setIntPref( + "network.proxy.type", + Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM + ); + + // clear cache + evict_cache_entries(); + + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/target" + ); + chan.asyncOpen(new ChannelListener(checkValue, null)); + + do_test_pending(); +} + +var called = false, + failed = false; +function redirect(metadata, response) { + // If called second time, just return the PAC but set failed-flag + if (called) { + failed = true; + return pac(metadata, response); + } + + called = true; + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", "/pac", false); + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); +} + +function pac(metadata, response) { + var PAC = 'function FindProxyForURL(url, host) { return "DIRECT"; }'; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader( + "Content-Type", + "application/x-ns-proxy-autoconfig", + false + ); + response.bodyOutputStream.write(PAC, PAC.length); +} + +function target(metadata, response) { + var retval = "ok"; + if (failed) { + retval = "failed"; + } + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(retval, retval.length); +} diff --git a/netwerk/test/unit/test_bug596443.js b/netwerk/test/unit/test_bug596443.js new file mode 100644 index 0000000000..07e4bd760b --- /dev/null +++ b/netwerk/test/unit/test_bug596443.js @@ -0,0 +1,115 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +var httpserver = new HttpServer(); + +var expectedOnStopRequests = 3; + +function setupChannel(suffix, xRequest, flags) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + if (flags) { + chan.loadFlags |= flags; + } + + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.setRequestHeader("x-request", xRequest, false); + + return httpChan; +} + +function Listener(response) { + this._response = response; +} +Listener.prototype = { + _response: null, + _buffer: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._buffer = ""; + }, + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest(request, status) { + Assert.equal(this._buffer, this._response); + if (--expectedOnStopRequests == 0) { + do_timeout(10, function() { + httpserver.stop(do_test_finished); + }); + } + }, +}; + +function run_test() { + httpserver.registerPathHandler("/bug596443", handler); + httpserver.start(-1); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + // make sure we have a profile so we can use the disk-cache + do_get_profile(); + + // clear cache + evict_cache_entries(); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var ch0 = setupChannel( + "/bug596443", + "Response0", + Ci.nsIRequest.LOAD_BYPASS_CACHE + ); + ch0.asyncOpen(new Listener("Response0")); + + var ch1 = setupChannel( + "/bug596443", + "Response1", + Ci.nsIRequest.LOAD_BYPASS_CACHE + ); + ch1.asyncOpen(new Listener("Response1")); + + var ch2 = setupChannel("/bug596443", "Should not be used"); + ch2.asyncOpen(new Listener("Response1")); // Note param: we expect this to come from cache + }); + + do_test_pending(); +} + +function triggerHandlers() { + do_timeout(100, handlers[1]); + do_timeout(100, handlers[0]); +} + +var handlers = []; +function handler(metadata, response) { + var func = function(body) { + return function() { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Length", "" + body.length, false); + response.setHeader("Cache-Control", "max-age=600", false); + response.bodyOutputStream.write(body, body.length); + response.finish(); + }; + }; + + response.processAsync(); + var request = metadata.getHeader("x-request"); + handlers.push(func(request)); + + if (handlers.length > 1) { + triggerHandlers(); + } +} diff --git a/netwerk/test/unit/test_bug618835.js b/netwerk/test/unit/test_bug618835.js new file mode 100644 index 0000000000..bc9de4e519 --- /dev/null +++ b/netwerk/test/unit/test_bug618835.js @@ -0,0 +1,122 @@ +// +// If a response to a non-safe HTTP request-method contains the Location- or +// Content-Location header, we must make sure to invalidate any cached entry +// representing the URIs pointed to by either header. RFC 2616 section 13.10 +// +// This test uses 3 URIs: "/post" is the target of a POST-request and always +// redirects (301) to "/redirect". The URIs "/redirect" and "/cl" both counts +// the number of loads from the server (handler). The response from "/post" +// always contains the headers "Location: /redirect" and "Content-Location: +// /cl", whose cached entries are to be invalidated. The tests verifies that +// "/redirect" and "/cl" are loaded from server the expected number of times. +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function setupChannel(path) { + return NetUtil.newChannel({ + uri: path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// Verify that Content-Location-URI has been loaded once, load post_target +function InitialListener() {} +InitialListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(1, numberOfCLHandlerCalls); + executeSoon(function() { + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/post" + ); + channel.requestMethod = "POST"; + channel.asyncOpen(new RedirectingListener()); + }); + }, +}; + +// Verify that Location-URI has been loaded once, reload post_target +function RedirectingListener() {} +RedirectingListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(1, numberOfHandlerCalls); + executeSoon(function() { + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/post" + ); + channel.requestMethod = "POST"; + channel.asyncOpen(new VerifyingListener()); + }); + }, +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// reload Content-Location-URI +function VerifyingListener() {} +VerifyingListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(2, numberOfHandlerCalls); + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/cl" + ); + channel.asyncOpen(new FinalListener()); + }, +}; + +// Verify that Location-URI has been loaded twice (cached entry invalidated), +// stop test +function FinalListener() {} +FinalListener.prototype = { + onStartRequest(request) {}, + onStopRequest(request, status) { + Assert.equal(2, numberOfCLHandlerCalls); + httpserv.stop(do_test_finished); + }, +}; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cl", content_location); + httpserv.registerPathHandler("/post", post_target); + httpserv.registerPathHandler("/redirect", redirect_target); + httpserv.start(-1); + + // Clear cache + evict_cache_entries(); + + // Load Content-Location URI into cache and start the chain of loads + var channel = setupChannel( + "http://localhost:" + httpserv.identity.primaryPort + "/cl" + ); + channel.asyncOpen(new InitialListener()); + + do_test_pending(); +} + +var numberOfCLHandlerCalls = 0; +function content_location(metadata, response) { + numberOfCLHandlerCalls++; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=360000", false); +} + +function post_target(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "/redirect", false); + response.setHeader("Content-Location", "/cl", false); + response.setHeader("Cache-Control", "max-age=360000", false); +} + +var numberOfHandlerCalls = 0; +function redirect_target(metadata, response) { + numberOfHandlerCalls++; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Cache-Control", "max-age=360000", false); +} diff --git a/netwerk/test/unit/test_bug633743.js b/netwerk/test/unit/test_bug633743.js new file mode 100644 index 0000000000..1df40c3a48 --- /dev/null +++ b/netwerk/test/unit/test_bug633743.js @@ -0,0 +1,193 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const VALUE_HDR_NAME = "X-HTTP-VALUE-HEADER"; +const VARY_HDR_NAME = "X-HTTP-VARY-HEADER"; +const CACHECTRL_HDR_NAME = "X-CACHE-CONTROL-HEADER"; + +var httpserver = null; + +function make_channel(flags, vary, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/bug633743", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan.QueryInterface(Ci.nsIHttpChannel); +} + +function Test(flags, varyHdr, sendValue, expectValue, cacheHdr) { + this._flags = flags; + this._varyHdr = varyHdr; + this._sendVal = sendValue; + this._expectVal = expectValue; + this._cacheHdr = cacheHdr; +} + +Test.prototype = { + _buffer: "", + _flags: null, + _varyHdr: null, + _sendVal: null, + _expectVal: null, + _cacheHdr: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) {}, + + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(this._buffer, this._expectVal); + do_timeout(0, run_next_test); + }, + + run() { + var channel = make_channel(); + channel.loadFlags = this._flags; + channel.setRequestHeader(VALUE_HDR_NAME, this._sendVal, false); + channel.setRequestHeader(VARY_HDR_NAME, this._varyHdr, false); + if (this._cacheHdr) { + channel.setRequestHeader(CACHECTRL_HDR_NAME, this._cacheHdr, false); + } + + channel.asyncOpen(this); + }, +}; + +var gTests = [ + // Test LOAD_FROM_CACHE: Load cache-entry + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-initial", // hdr-value used to vary + "request1", // echoed by handler + "request1" // value expected to receive in channel + ), + // Verify that it was cached + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-initial", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request1" // value expected to receive in channel + ), + // Load same entity with LOAD_FROM_CACHE-flag + new Test( + Ci.nsIRequest.LOAD_FROM_CACHE, + "entity-initial", // hdr-value used to vary + "fresh value with LOAD_FROM_CACHE", // echoed by handler + "request1" // value expected to receive in channel + ), + // Load different entity with LOAD_FROM_CACHE-flag + new Test( + Ci.nsIRequest.LOAD_FROM_CACHE, + "entity-l-f-c", // hdr-value used to vary + "request2", // echoed by handler + "request2" // value expected to receive in channel + ), + // Verify that new value was cached + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-l-f-c", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request2" // value expected to receive in channel + ), + + // Test VALIDATE_NEVER: Note previous cache-entry + new Test( + Ci.nsIRequest.VALIDATE_NEVER, + "entity-v-n", // hdr-value used to vary + "request3", // echoed by handler + "request3" // value expected to receive in channel + ), + // Verify that cache-entry was replaced + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-v-n", // hdr-value used to vary + "fresh value with LOAD_NORMAL", // echoed by handler + "request3" // value expected to receive in channel + ), + + // Test combination VALIDATE_NEVER && no-store: Load new cache-entry + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-2", // hdr-value used to vary + "request4", // echoed by handler + "request4", // value expected to receive in channel + "no-store" // set no-store on response + ), + // Ensure we validate without IMS header in this case (verified in handler) + new Test( + Ci.nsIRequest.VALIDATE_NEVER, + "entity-2-v-n", // hdr-value used to vary + "request5", // echoed by handler + "request5" // value expected to receive in channel + ), + + // Test VALIDATE-ALWAYS: Load new entity + new Test( + Ci.nsIRequest.LOAD_NORMAL, + "entity-3", // hdr-value used to vary + "request6", // echoed by handler + "request6", // value expected to receive in channel + "no-cache" // set no-cache on response + ), + // Ensure we don't send IMS header also in this case (verified in handler) + new Test( + Ci.nsIRequest.VALIDATE_ALWAYS, + "entity-3-v-a", // hdr-value used to vary + "request7", // echoed by handler + "request7" // value expected to receive in channel + ), +]; + +function run_next_test() { + if (gTests.length == 0) { + httpserver.stop(do_test_finished); + return; + } + + var test = gTests.shift(); + test.run(); +} + +function handler(metadata, response) { + // None of the tests above should send an IMS + Assert.ok(!metadata.hasHeader("If-Modified-Since")); + + // Pick up requested value to echo + var hdr = "default value"; + try { + hdr = metadata.getHeader(VALUE_HDR_NAME); + } catch (ex) {} + + // Pick up requested cache-control header-value + var cctrlVal = "max-age=10000"; + try { + cctrlVal = metadata.getHeader(CACHECTRL_HDR_NAME); + } catch (ex) {} + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", cctrlVal, false); + response.setHeader("Vary", VARY_HDR_NAME, false); + response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false); + response.bodyOutputStream.write(hdr, hdr.length); +} + +function run_test() { + // clear the cache + evict_cache_entries(); + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/bug633743", handler); + httpserver.start(-1); + + run_next_test(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug650522.js b/netwerk/test/unit/test_bug650522.js new file mode 100644 index 0000000000..b7ad89b1c1 --- /dev/null +++ b/netwerk/test/unit/test_bug650522.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", false); + + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + var expiry = (Date.now() + 1000) * 1000; + + // Test our handling of host names with a single character at the beginning + // followed by a dot. + cm.add( + "e.com", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + Assert.equal(cm.countCookiesFromHost("e.com"), 1); + + CookieXPCShellUtils.createServer({ hosts: ["e.com"] }); + const cookies = await CookieXPCShellUtils.getCookieStringFromDocument( + "http://e.com/" + ); + Assert.equal(cookies, "foo=bar"); +}); diff --git a/netwerk/test/unit/test_bug650995.js b/netwerk/test/unit/test_bug650995.js new file mode 100644 index 0000000000..73a004390a --- /dev/null +++ b/netwerk/test/unit/test_bug650995.js @@ -0,0 +1,189 @@ +// +// Test that "max_entry_size" prefs for disk- and memory-cache prevents +// caching resources with size out of bounds +// + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +do_get_profile(); + +const prefService = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +const httpserver = new HttpServer(); + +// Repeats the given data until the total size is larger than 1K +function repeatToLargerThan1K(data) { + while (data.length <= 1024) { + data += data; + } + return data; +} + +function setupChannel(suffix, value) { + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + httpChan.setRequestHeader("x-request", value, false); + + return httpChan; +} + +var tests = [ + new InitializeCacheDevices(true, false), // enable and create mem-device + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.memory.max_entry_size", 1); + }, + "012345", + "9876543210", + "012345" + ), // expect cached value + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.memory.max_entry_size", 1); + }, + "0123456789a", + "9876543210", + "9876543210" + ), // expect fresh value + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.memory.max_entry_size", -1); + }, + "0123456789a", + "9876543210", + "0123456789a" + ), // expect cached value + + new InitializeCacheDevices(false, true), // enable and create disk-device + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.disk.max_entry_size", 1); + }, + "012345", + "9876543210", + "012345" + ), // expect cached value + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.disk.max_entry_size", 1); + }, + "0123456789a", + "9876543210", + "9876543210" + ), // expect fresh value + new TestCacheEntrySize( + function() { + prefService.setIntPref("browser.cache.disk.max_entry_size", -1); + }, + "0123456789a", + "9876543210", + "0123456789a" + ), // expect cached value +]; + +function nextTest() { + // We really want each test to be self-contained. Make sure cache is + // cleared and also let all operations finish before starting a new test + syncWithCacheIOThread(function() { + get_cache_service().clear(); + syncWithCacheIOThread(runNextTest); + }); +} + +function runNextTest() { + var aTest = tests.shift(); + if (!aTest) { + httpserver.stop(do_test_finished); + return; + } + executeSoon(function() { + aTest.start(); + }); +} + +// Just make sure devices are created +function InitializeCacheDevices(memDevice, diskDevice) { + this.start = function() { + prefService.setBoolPref("browser.cache.memory.enable", memDevice); + if (memDevice) { + let cap = prefService.getIntPref("browser.cache.memory.capacity", 0); + if (cap == 0) { + prefService.setIntPref("browser.cache.memory.capacity", 1024); + } + } + prefService.setBoolPref("browser.cache.disk.enable", diskDevice); + if (diskDevice) { + let cap = prefService.getIntPref("browser.cache.disk.capacity", 0); + if (cap == 0) { + prefService.setIntPref("browser.cache.disk.capacity", 1024); + } + } + var channel = setupChannel("/bug650995", "Initial value"); + channel.asyncOpen(new ChannelListener(nextTest, null)); + }; +} + +function TestCacheEntrySize( + setSizeFunc, + firstRequest, + secondRequest, + secondExpectedReply +) { + // Initially, this test used 10 bytes as the limit for caching entries. + // Since we now use 1K granularity we have to extend lengths to be larger + // than 1K if it is larger than 10 + if (firstRequest.length > 10) { + firstRequest = repeatToLargerThan1K(firstRequest); + } + if (secondExpectedReply.length > 10) { + secondExpectedReply = repeatToLargerThan1K(secondExpectedReply); + } + + this.start = function() { + setSizeFunc(); + var channel = setupChannel("/bug650995", firstRequest); + channel.asyncOpen(new ChannelListener(this.initialLoad, this)); + }; + this.initialLoad = function(request, data, ctx) { + Assert.equal(firstRequest, data); + var channel = setupChannel("/bug650995", secondRequest); + executeSoon(function() { + channel.asyncOpen(new ChannelListener(ctx.testAndTriggerNext, ctx)); + }); + }; + this.testAndTriggerNext = function(request, data, ctx) { + Assert.equal(secondExpectedReply, data); + executeSoon(nextTest); + }; +} + +function run_test() { + httpserver.registerPathHandler("/bug650995", handler); + httpserver.start(-1); + + prefService.setBoolPref("browser.cache.offline.enable", false); + prefService.setBoolPref("browser.cache.offline.storage.enable", false); + prefService.setBoolPref("network.http.rcwn.enabled", false); + + nextTest(); + do_test_pending(); +} + +function handler(metadata, response) { + var body = "BOOM!"; + try { + body = metadata.getHeader("x-request"); + } catch (e) {} + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "max-age=3600", false); + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_bug652761.js b/netwerk/test/unit/test_bug652761.js new file mode 100644 index 0000000000..030f18a1fd --- /dev/null +++ b/netwerk/test/unit/test_bug652761.js @@ -0,0 +1,19 @@ +// This is just a crashtest for a url that is rejected at parse time (port 80,000) + +"use strict"; + +function run_test() { + // Bug 1301621 makes invalid ports throw + Assert.throws( + () => { + NetUtil.newChannel({ + uri: "http://localhost:80000/", + loadUsingSystemPrincipal: true, + }); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port" + ); + + do_test_finished(); +} diff --git a/netwerk/test/unit/test_bug654926.js b/netwerk/test/unit/test_bug654926.js new file mode 100644 index 0000000000..3a5660dfdb --- /dev/null +++ b/netwerk/test/unit/test_bug654926.js @@ -0,0 +1,102 @@ +"use strict"; + +var _PSvc; +function get_pref_service() { + if (_PSvc) { + return _PSvc; + } + + return (_PSvc = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + )); +} + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + // write 2MiB + var i; + for (i = 0; i < 2; i++) { + write_and_check(os, data, data.length); + } + + os.close(); + entry.close(); + + // now change max_entry_size so that the existing entry is too big + get_pref_service().setIntPref("browser.cache.disk.max_entry_size", 1024); + + // append to entry + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + append_datafile + ); +} + +function append_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize, -1); + var data = gen_1MiB(); + + // append 1MiB + try { + write_and_check(os, data, data.length); + do_throw(); + } catch (ex) {} + + // closing the ostream should fail in this case + try { + os.close(); + do_throw(); + } catch (ex) {} + + entry.close(); + + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug654926_doom_and_read.js b/netwerk/test/unit/test_bug654926_doom_and_read.js new file mode 100644 index 0000000000..641b1d233e --- /dev/null +++ b/netwerk/test/unit/test_bug654926_doom_and_read.js @@ -0,0 +1,90 @@ +"use strict"; + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function make_input_stream_scriptable(input) { + var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + wrapper.init(input); + return wrapper; +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + write_and_check(os, data, data.length); + + os.close(); + entry.close(); + + // open, doom, append, read + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + test_read_after_doom + ); +} + +function test_read_after_doom(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(entry.dataSize, data.length); + + entry.asyncDoom(null); + write_and_check(os, data, data.length); + + os.close(); + + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + Assert.equal(read.length, 2 * 1024 * 1024); + is.close(); + + entry.close(); + do_test_finished(); + }); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug654926_test_seek.js b/netwerk/test/unit/test_bug654926_test_seek.js new file mode 100644 index 0000000000..148e9f9043 --- /dev/null +++ b/netwerk/test/unit/test_bug654926_test_seek.js @@ -0,0 +1,76 @@ +"use strict"; + +function gen_1MiB() { + var i; + var data = "x"; + for (i = 0; i < 20; i++) { + data += data; + } + return data; +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_datafile(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_1MiB(); + var os = entry.openOutputStream(0, data.length); + + write_and_check(os, data, data.length); + + os.close(); + entry.close(); + + // try to open the entry for appending + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + open_for_readwrite + ); +} + +function open_for_readwrite(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.dataSize, -1); + + // Opening the entry for appending data calls nsDiskCacheStreamIO::Seek() + // which initializes mFD. If no data is written then mBufDirty is false and + // mFD won't be closed in nsDiskCacheStreamIO::Flush(). + + os.close(); + entry.close(); + + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + write_datafile + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug659569.js b/netwerk/test/unit/test_bug659569.js new file mode 100644 index 0000000000..60a14765b2 --- /dev/null +++ b/netwerk/test/unit/test_bug659569.js @@ -0,0 +1,58 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); + +function setupChannel(suffix) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + suffix, + loadUsingSystemPrincipal: true, + }); +} + +function checkValueAndTrigger(request, data, ctx) { + Assert.equal("Ok", data); + httpserver.stop(do_test_finished); +} + +function run_test() { + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + httpserver.registerPathHandler("/redirect1", redirectHandler1); + httpserver.registerPathHandler("/redirect2", redirectHandler2); + httpserver.start(-1); + + // clear cache + evict_cache_entries(); + + // load first time + var channel = setupChannel("/redirect1"); + channel.asyncOpen(new ChannelListener(checkValueAndTrigger, null)); + do_test_pending(); +} + +function redirectHandler1(metadata, response) { + if (!metadata.hasHeader("Cookie")) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "max-age=600", false); + response.setHeader("Location", "/redirect2?query", false); + response.setHeader("Set-Cookie", "MyCookie=1", false); + } else { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); + } +} + +function redirectHandler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader("Location", "/redirect1", false); +} diff --git a/netwerk/test/unit/test_bug660066.js b/netwerk/test/unit/test_bug660066.js new file mode 100644 index 0000000000..00abe8de84 --- /dev/null +++ b/netwerk/test/unit/test_bug660066.js @@ -0,0 +1,56 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +"use strict"; + +const SIMPLEURI_SPEC = "data:text/plain,hello world"; +const BLOBURI_SPEC = "blob:123456"; + +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "\n" + + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} + +function do_check_uri_neq(uri1, uri2) { + do_info("Checking equality in forward direction..."); + Assert.ok(!uri1.equals(uri2)); + Assert.ok(!uri1.equalsExceptRef(uri2)); + + do_info("Checking equality in reverse direction..."); + Assert.ok(!uri2.equals(uri1)); + Assert.ok(!uri2.equalsExceptRef(uri1)); +} + +function run_test() { + var simpleURI = NetUtil.newURI(SIMPLEURI_SPEC); + var fileDataURI = NetUtil.newURI(BLOBURI_SPEC); + + do_info("Checking that " + SIMPLEURI_SPEC + " != " + BLOBURI_SPEC); + do_check_uri_neq(simpleURI, fileDataURI); + + do_info("Changing the nsSimpleURI spec to match the nsFileDataURI"); + simpleURI = simpleURI + .mutate() + .setSpec(BLOBURI_SPEC) + .finalize(); + + do_info("Verifying that .spec matches"); + Assert.equal(simpleURI.spec, fileDataURI.spec); + + do_info( + "Checking that nsSimpleURI != nsFileDataURI despite their .spec matching" + ); + do_check_uri_neq(simpleURI, fileDataURI); +} diff --git a/netwerk/test/unit/test_bug667087.js b/netwerk/test/unit/test_bug667087.js new file mode 100644 index 0000000000..b3d87402e1 --- /dev/null +++ b/netwerk/test/unit/test_bug667087.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async () => { + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + var expiry = (Date.now() + 1000) * 1000; + + // Test our handling of host names with a single character consisting only + // of a single character + cm.add( + "a", + "/", + "foo", + "bar", + false, + false, + true, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTP + ); + Assert.equal(cm.countCookiesFromHost("a"), 1); + + CookieXPCShellUtils.createServer({ hosts: ["a"] }); + const cookies = await CookieXPCShellUtils.getCookieStringFromDocument( + "http://a/" + ); + Assert.equal(cookies, "foo=bar"); +}); diff --git a/netwerk/test/unit/test_bug667818.js b/netwerk/test/unit/test_bug667818.js new file mode 100644 index 0000000000..1ec185c832 --- /dev/null +++ b/netwerk/test/unit/test_bug667818.js @@ -0,0 +1,50 @@ +"use strict"; + +function makeURI(str) { + return Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(str); +} + +add_task(async () => { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + var serv = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + var uri = makeURI("http://example.com/"); + var channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + CookieXPCShellUtils.createServer({ hosts: ["example.com"] }); + + // Try an expiration time before the epoch + + await CookieXPCShellUtils.setCookieToDocument( + uri.spec, + "test=test; path=/; domain=example.com; expires=Sun, 31-Dec-1899 16:00:00 GMT;" + ); + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec), + "" + ); + + // Now sanity check + serv.setCookieStringFromHttp( + uri, + "test2=test2; path=/; domain=example.com;", + channel + ); + + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec), + "test2=test2" + ); +}); diff --git a/netwerk/test/unit/test_bug667907.js b/netwerk/test/unit/test_bug667907.js new file mode 100644 index 0000000000..689dcd0886 --- /dev/null +++ b/netwerk/test/unit/test_bug667907.js @@ -0,0 +1,86 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; +var simplePath = "/simple"; +var normalPath = "/normal"; +var httpbody = ""; + +XPCOMUtils.defineLazyGetter(this, "uri1", function() { + return "http://localhost:" + httpserver.identity.primaryPort + simplePath; +}); + +XPCOMUtils.defineLazyGetter(this, "uri2", function() { + return "http://localhost:" + httpserver.identity.primaryPort + normalPath; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var listener_proto = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + Assert.equal( + request.QueryInterface(Ci.nsIChannel).contentType, + this.contentType + ); + request.cancel(Cr.NS_BINDING_ABORTED); + }, + + onDataAvailable(request, stream, offset, count) { + do_throw("Unexpected onDataAvailable"); + }, + + onStopRequest(request, status) { + Assert.equal(status, Cr.NS_BINDING_ABORTED); + this.termination_func(); + }, +}; + +function listener(contentType, termination_func) { + this.contentType = contentType; + this.termination_func = termination_func; +} +listener.prototype = listener_proto; + +function run_test() { + httpserver = new HttpServer(); + httpserver.registerPathHandler(simplePath, simpleHandler); + httpserver.registerPathHandler(normalPath, normalHandler); + httpserver.start(-1); + + var channel = make_channel(uri1); + channel.asyncOpen( + new listener("text/plain", function() { + run_test2(); + }) + ); + + do_test_pending(); +} + +function run_test2() { + var channel = make_channel(uri2); + channel.asyncOpen( + new listener("text/html", function() { + httpserver.stop(do_test_finished); + }) + ); +} + +function simpleHandler(metadata, response) { + response.seizePower(); + response.bodyOutputStream.write(httpbody, httpbody.length); + response.finish(); +} + +function normalHandler(metadata, response) { + response.bodyOutputStream.write(httpbody, httpbody.length); + response.finish(); +} diff --git a/netwerk/test/unit/test_bug669001.js b/netwerk/test/unit/test_bug669001.js new file mode 100644 index 0000000000..bb16f0a5d7 --- /dev/null +++ b/netwerk/test/unit/test_bug669001.js @@ -0,0 +1,179 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +var path = "/bug699001"; + +XPCOMUtils.defineLazyGetter(this, "URI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + path; +}); + +function make_channel(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var fetched; + +// The test loads a resource that expires in one year, has an etag and varies only by User-Agent +// First we load it, then check we load it only from the cache w/o even checking with the server +// Then we modify our User-Agent and try it again +// We have to get a new content (even though with the same etag) and again on next load only from +// cache w/o accessing the server +// Goal is to check we've updated User-Agent request header in cache after we've got 304 response +// from the server + +var tests = [ + { + prepare() {}, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, + { + prepare() { + setUA("A different User Agent"); + }, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, + { + prepare() { + setUA("And another User Agent"); + }, + test(response) { + Assert.ok(fetched); + }, + }, + { + prepare() {}, + test(response) { + Assert.ok(!fetched); + }, + }, +]; + +function handler(metadata, response) { + if (metadata.hasHeader("If-None-Match")) { + response.setStatusLine(metadata.httpVersion, 304, "Not modified"); + } else { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + + var body = "body"; + response.bodyOutputStream.write(body, body.length); + } + + fetched = true; + + response.setHeader("Expires", getDateString(+1)); + response.setHeader("Cache-Control", "private"); + response.setHeader("Vary", "User-Agent"); + response.setHeader("ETag", "1234"); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(path, handler); + httpServer.start(-1); + + do_test_pending(); + + nextTest(); +} + +function nextTest() { + fetched = false; + tests[0].prepare(); + + dump("Testing with User-Agent: " + getUA() + "\n"); + var chan = make_channel(URI); + + // Give the old channel a chance to close the cache entry first. + // XXX This is actually a race condition that might be considered a bug... + executeSoon(function() { + chan.asyncOpen(new ChannelListener(checkAndShiftTest, null)); + }); +} + +function checkAndShiftTest(request, response) { + tests[0].test(response); + + tests.shift(); + if (tests.length == 0) { + httpServer.stop(tearDown); + return; + } + + nextTest(); +} + +function tearDown() { + setUA(""); + do_test_finished(); +} + +// Helpers + +function getUA() { + var httphandler = Cc["@mozilla.org/network/protocol;1?name=http"].getService( + Ci.nsIHttpProtocolHandler + ); + return httphandler.userAgent; +} + +function setUA(value) { + var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setCharPref("general.useragent.override", value); +} + +function getDateString(yearDelta) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + (d.getUTCFullYear() + yearDelta) + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_bug767025.js b/netwerk/test/unit/test_bug767025.js new file mode 100644 index 0000000000..cc10d85ae2 --- /dev/null +++ b/netwerk/test/unit/test_bug767025.js @@ -0,0 +1,315 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +/** + * This testcase does the following steps to make sure that bug767025 removes + * files as expected. + * + * STEPS: + * - Schedule a offline cache update for app.manifest. + * - pages/foo1, pages/foo2, pages/foo3, and pages/foo4 are cached. + * - Activate pages/foo1 + * - Doom pages/foo1, and pages/foo2. + * - pages/foo1 should keep alive while pages/foo2 was gone. + * - Activate pages/foo3 + * - Evict all documents. + * - all documents except pages/foo1 are gone since pages/foo1 & pages/foo3 + * are activated. + */ + +const kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID = + "@mozilla.org/offlinecacheupdate-service;1"; +const kNS_CACHESTORAGESERVICE_CONTRACTID = + "@mozilla.org/netwerk/cache-storage-service;1"; +const kNS_APPLICATIONCACHESERVICE_CONTRACTID = + "@mozilla.org/network/application-cache-service;1"; + +const kManifest = + "CACHE MANIFEST\n" + + "/pages/foo1\n" + + "/pages/foo2\n" + + "/pages/foo3\n" + + "/pages/foo4\n"; + +const kDataFileSize = 1024; // file size for each content page +const kHttpLocation = "http://localhost:4444/"; + +function manifest_handler(metadata, response) { + info("manifest\n"); + response.setHeader("content-type", "text/cache-manifest"); + + response.write(kManifest); +} + +function datafile_handler(metadata, response) { + info("datafile_handler\n"); + let data = ""; + + while (data.length < kDataFileSize) { + data = + data + + Math.random() + .toString(36) + .substring(2, 15); + } + + response.setHeader("content-type", "text/plain"); + response.write(data.substring(0, kDataFileSize)); +} + +function app_handler(metadata, response) { + info("app_handler\n"); + response.setHeader("content-type", "text/html"); + + response.write(""); +} + +var httpServer; + +function init_profile() { + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + info("profile " + do_get_profile()); +} + +function init_http_server() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/app.appcache", manifest_handler); + httpServer.registerPathHandler("/app", app_handler); + for (let i = 1; i <= 4; i++) { + httpServer.registerPathHandler("/pages/foo" + i, datafile_handler); + } + httpServer.start(4444); +} + +function clean_app_cache() { + let cache_service = Cc[kNS_CACHESTORAGESERVICE_CONTRACTID].getService( + Ci.nsICacheStorageService + ); + let storage = cache_service.appCacheStorage( + Services.loadContextInfo.default, + null + ); + storage.asyncEvictStorage(null); +} + +function do_app_cache(manifestURL, pageURL) { + let update_service = Cc[kNS_OFFLINECACHEUPDATESERVICE_CONTRACTID].getService( + Ci.nsIOfflineCacheUpdateService + ); + + PermissionTestUtils.add( + manifestURL, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + let update = update_service.scheduleUpdate( + manifestURL, + pageURL, + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); /* no window */ + + return update; +} + +function watch_update(update, stateChangeHandler, cacheAvailHandler) { + let observer = { + QueryInterface: ChromeUtils.generateQI([]), + + updateStateChanged: stateChangeHandler, + applicationCacheAvailable: cacheAvailHandler, + }; + ~update.addObserver(observer); + + return update; +} + +function start_and_watch_app_cache( + manifestURL, + pageURL, + stateChangeHandler, + cacheAvailHandler +) { + let ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService + ); + let update = do_app_cache( + ioService.newURI(manifestURL), + ioService.newURI(pageURL) + ); + watch_update(update, stateChangeHandler, cacheAvailHandler); + return update; +} + +const { + STATE_FINISHED: STATE_FINISHED, + STATE_CHECKING: STATE_CHECKING, + STATE_ERROR: STATE_ERROR, +} = Ci.nsIOfflineCacheUpdateObserver; + +/* + * Start caching app1 as a non-pinned app. + */ +function start_cache_nonpinned_app() { + info("Start non-pinned App1"); + start_and_watch_app_cache( + kHttpLocation + "app.appcache", + kHttpLocation + "app", + function(update, state) { + switch (state) { + case STATE_FINISHED: + check_bug(); + break; + + case STATE_ERROR: + do_throw("App cache state = " + state); + break; + } + }, + function(appcache) { + info("app avail " + appcache + "\n"); + } + ); +} + +var hold_entry_foo1 = null; + +function check_bug() { + // activate foo1 + asyncOpenCacheEntry( + kHttpLocation + "pages/foo1", + "appcache", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(status, entry, appcache) { + let storage = get_cache_service().appCacheStorage( + Services.loadContextInfo.default, + appcache + ); + + // Doom foo1 & foo2 + storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo1"), "", { + onCacheEntryDoomed() { + storage.asyncDoomURI(createURI(kHttpLocation + "pages/foo2"), "", { + onCacheEntryDoomed() { + check_evict_cache(appcache); + }, + }); + }, + }); + + hold_entry_foo1 = entry; + } + ); +} + +function check_evict_cache(appcache) { + // Only foo2 should be removed. + let file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("5"); + file.append("9"); + file.append("8379C6596B8CA4-0"); + Assert.equal(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("C"); + file.append("2"); + file.append("5F356A168B5E3B-0"); + Assert.equal(file.exists(), false); + + // activate foo3 + asyncOpenCacheEntry( + kHttpLocation + "pages/foo3", + "appcache", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(status, entry, appcache) { + // evict all documents. + let storage = get_cache_service().appCacheStorage( + Services.loadContextInfo.default, + appcache + ); + storage.asyncEvictStorage(null); + + // All documents are removed except foo1 & foo3. + syncWithCacheIOThread(function() { + // foo1 + let file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("5"); + file.append("9"); + file.append("8379C6596B8CA4-0"); + Assert.equal(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("0"); + file.append("0"); + file.append("61FEE819921D39-0"); + Assert.equal(file.exists(), false); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("3"); + file.append("9"); + file.append("0D8759F1DE5452-0"); + Assert.equal(file.exists(), false); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("C"); + file.append("2"); + file.append("5F356A168B5E3B-0"); + Assert.equal(file.exists(), false); + + // foo3 + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("D"); + file.append("C"); + file.append("1ADCCC843B5C00-0"); + Assert.equal(file.exists(), true); + + file = do_get_profile().clone(); + file.append("OfflineCache"); + file.append("F"); + file.append("0"); + file.append("FC3E6D6C1164E9-0"); + Assert.equal(file.exists(), false); + + httpServer.stop(do_test_finished); + }, true /* force even with the new cache back end */); + }, + appcache + ); +} + +function run_test() { + if (typeof _XPCSHELL_PROCESS == "undefined" || _XPCSHELL_PROCESS != "child") { + init_profile(); + clean_app_cache(); + } + + init_http_server(); + start_cache_nonpinned_app(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug770243.js b/netwerk/test/unit/test_bug770243.js new file mode 100644 index 0000000000..5c79d6b1c1 --- /dev/null +++ b/netwerk/test/unit/test_bug770243.js @@ -0,0 +1,246 @@ +/* this test does the following: + Always requests the same resource, while for each request getting: + 1. 200 + ETag: "one" + 2. 401 followed by 200 + ETag: "two" + 3. 401 followed by 304 + 4. 407 followed by 200 + ETag: "three" + 5. 407 followed by 304 +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv; + +function addCreds(scheme, host) { + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.setAuthIdentity( + scheme, + host, + httpserv.identity.primaryPort, + "basic", + "secret", + "/", + "", + "user", + "pass" + ); +} + +function clearCreds() { + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); +} + +function makeChan() { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserv.identity.primaryPort + "/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +// Array of handlers that are called one by one in response to expected requests + +var handlers = [ + // Test 1 + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"one"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 1"; + response.bodyOutputStream.write(body, body.length); + }, + + // Test 2 + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"one"'); + response.setStatusLine(metadata.httpVersion, 401, "Authenticate"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), true); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"two"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 2"; + response.bodyOutputStream.write(body, body.length); + clearCreds(); + }, + + // Test 3 + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 401, "Authenticate"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 304, "OK"); + response.setHeader("ETag", '"two"', false); + clearCreds(); + }, + + // Test 4 + function(metadata, response) { + Assert.equal(metadata.hasHeader("Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate"); + response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function(metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"two"'); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("ETag", '"three"', false); + response.setHeader("Cache-control", "no-cache", false); + response.setHeader("Content-type", "text/plain", false); + var body = "Response body 3"; + response.bodyOutputStream.write(body, body.length); + clearCreds(); + }, + + // Test 5 + function(metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), false); + Assert.equal(metadata.getHeader("If-None-Match"), '"three"'); + response.setStatusLine(metadata.httpVersion, 407, "Proxy Authenticate"); + response.setHeader("Proxy-Authenticate", 'Basic realm="secret"', false); + addCreds("http", "localhost"); + }, + function(metadata, response) { + Assert.equal(metadata.hasHeader("Proxy-Authorization"), true); + Assert.equal(metadata.getHeader("If-None-Match"), '"three"'); + response.setStatusLine(metadata.httpVersion, 304, "OK"); + response.setHeader("ETag", '"three"', false); + response.setHeader("Cache-control", "no-cache", false); + clearCreds(); + }, +]; + +function handler(metadata, response) { + handlers.shift()(metadata, response); +} + +// Array of tests to run, self-driven + +function sync_and_run_next_test() { + syncWithCacheIOThread(function() { + tests.shift()(); + }); +} + +var tests = [ + // Test 1: 200 (cacheable) + function() { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function(req, body) { + Assert.equal(body, "Response body 1"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 2: 401 and 200 + new content + function() { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function(req, body) { + Assert.equal(body, "Response body 2"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 3: 401 and 304 + function() { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function(req, body) { + Assert.equal(body, "Response body 2"); + sync_and_run_next_test(); + }, + null, + CL_FROM_CACHE + ) + ); + }, + + // Test 4: 407 and 200 + new content + function() { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function(req, body) { + Assert.equal(body, "Response body 3"); + sync_and_run_next_test(); + }, + null, + CL_NOT_FROM_CACHE + ) + ); + }, + + // Test 5: 407 and 304 + function() { + var ch = makeChan(); + ch.asyncOpen( + new ChannelListener( + function(req, body) { + Assert.equal(body, "Response body 3"); + sync_and_run_next_test(); + }, + null, + CL_FROM_CACHE + ) + ); + }, + + // End of test run + function() { + httpserv.stop(do_test_finished); + }, +]; + +function run_test() { + do_get_profile(); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/", handler); + httpserv.start(-1); + + const prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + prefs.setCharPref("network.proxy.http", "localhost"); + prefs.setIntPref("network.proxy.http_port", httpserv.identity.primaryPort); + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + prefs.setIntPref("network.proxy.type", 1); + prefs.setBoolPref("network.http.rcwn.enabled", false); + + tests.shift()(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug812167.js b/netwerk/test/unit/test_bug812167.js new file mode 100644 index 0000000000..29bf894698 --- /dev/null +++ b/netwerk/test/unit/test_bug812167.js @@ -0,0 +1,141 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +/* +- get 302 with Cache-control: no-store +- check cache entry for the 302 response is cached only in memory device +- get 302 with Expires: -1 +- check cache entry for the 302 response is not cached at all +*/ + +var httpserver = null; +// Need to randomize, because apparently no one clears our cache +var randomPath1 = "/redirect-no-store/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI1", function() { + return "http://localhost:" + httpserver.identity.primaryPort + randomPath1; +}); + +var randomPath2 = "/redirect-expires-past/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI2", function() { + return "http://localhost:" + httpserver.identity.primaryPort + randomPath2; +}); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +var redirectHandler_NoStore_calls = 0; +function redirectHandler_NoStore(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader( + "Location", + "http://localhost:" + httpserver.identity.primaryPort + "/content", + false + ); + response.setHeader("Cache-control", "no-store"); + ++redirectHandler_NoStore_calls; +} + +var redirectHandler_ExpiresInPast_calls = 0; +function redirectHandler_ExpiresInPast(metadata, response) { + response.setStatusLine(metadata.httpVersion, 302, "Found"); + response.setHeader( + "Location", + "http://localhost:" + httpserver.identity.primaryPort + "/content", + false + ); + response.setHeader("Expires", "-1"); + ++redirectHandler_ExpiresInPast_calls; +} + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function check_response( + path, + request, + buffer, + expectedExpiration, + continuation +) { + Assert.equal(buffer, responseBody); + + // Entry is always there, old cache wrapping code does session->SetDoomEntriesIfExpired(false), + // just check it's not persisted or is expired (dep on the test). + asyncOpenCacheEntry( + path, + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + function(status, entry) { + Assert.equal(status, 0); + + // Expired entry is on disk, no-store entry is in memory + Assert.equal(entry.persistent, expectedExpiration); + + // Do the request again and check the server handler is called appropriately + var chan = make_channel(path); + chan.asyncOpen( + new ChannelListener(function(request, buffer) { + Assert.equal(buffer, responseBody); + + if (expectedExpiration) { + // Handler had to be called second time + Assert.equal(redirectHandler_ExpiresInPast_calls, 2); + } else { + // Handler had to be called second time (no-store forces validate), + // and we are just in memory + Assert.equal(redirectHandler_NoStore_calls, 2); + Assert.ok(!entry.persistent); + } + + continuation(); + }, null) + ); + } + ); +} + +function run_test_no_store() { + var chan = make_channel(randomURI1); + chan.asyncOpen( + new ChannelListener(function(request, buffer) { + // Cache-control: no-store response should only be found in the memory cache. + check_response(randomURI1, request, buffer, false, run_test_expires_past); + }, null) + ); +} + +function run_test_expires_past() { + var chan = make_channel(randomURI2); + chan.asyncOpen( + new ChannelListener(function(request, buffer) { + // Expires: -1 response should not be found in any cache. + check_response(randomURI2, request, buffer, true, finish_test); + }, null) + ); +} + +function finish_test() { + httpserver.stop(do_test_finished); +} + +function run_test() { + do_get_profile(); + + httpserver = new HttpServer(); + httpserver.registerPathHandler(randomPath1, redirectHandler_NoStore); + httpserver.registerPathHandler(randomPath2, redirectHandler_ExpiresInPast); + httpserver.registerPathHandler("/content", contentHandler); + httpserver.start(-1); + + run_test_no_store(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug826063.js b/netwerk/test/unit/test_bug826063.js new file mode 100644 index 0000000000..54e5d79a96 --- /dev/null +++ b/netwerk/test/unit/test_bug826063.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test that nsIPrivateBrowsingChannel.isChannelPrivate yields the correct + * result for various combinations of .setPrivate() and nsILoadContexts + */ + +"use strict"; + +var URIs = ["http://example.org", "https://example.org"]; +if (Services.prefs.getBoolPref("network.ftp.enabled")) { + URIs.push("ftp://example.org"); +} + +function* getChannels() { + for (let u of URIs) { + yield NetUtil.newChannel({ + uri: u, + loadUsingSystemPrincipal: true, + }); + } +} + +function checkPrivate(channel, shouldBePrivate) { + Assert.equal( + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel).isChannelPrivate, + shouldBePrivate + ); +} + +/** + * Default configuration + * Default is non-private + */ +add_test(function test_plain() { + for (let c of getChannels()) { + checkPrivate(c, false); + } + run_next_test(); +}); + +/** + * Explicitly setPrivate(true), no load context + */ +add_test(function test_setPrivate_private() { + for (let c of getChannels()) { + c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(true); + checkPrivate(c, true); + } + run_next_test(); +}); + +/** + * Explicitly setPrivate(false), no load context + */ +add_test(function test_setPrivate_regular() { + for (let c of getChannels()) { + c.QueryInterface(Ci.nsIPrivateBrowsingChannel).setPrivate(false); + checkPrivate(c, false); + } + run_next_test(); +}); + +/** + * Load context mandates private mode + */ +add_test(function test_LoadContextPrivate() { + let ctx = Cu.createPrivateLoadContext(); + for (let c of getChannels()) { + c.notificationCallbacks = ctx; + checkPrivate(c, true); + } + run_next_test(); +}); + +/** + * Load context mandates regular mode + */ +add_test(function test_LoadContextRegular() { + let ctx = Cu.createLoadContext(); + for (let c of getChannels()) { + c.notificationCallbacks = ctx; + checkPrivate(c, false); + } + run_next_test(); +}); + +// Do not test simultanous uses of .setPrivate and load context. +// There is little merit in doing so, and combining both will assert in +// Debug builds anyway. diff --git a/netwerk/test/unit/test_bug856978.js b/netwerk/test/unit/test_bug856978.js new file mode 100644 index 0000000000..071366e66f --- /dev/null +++ b/netwerk/test/unit/test_bug856978.js @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure that the authorization header can get deleted e.g. by +// extensions if they are observing "http-on-modify-request". In a first step +// the auth cache is filled with credentials which then get added to the +// following request. On "http-on-modify-request" it is tested whether the +// authorization header got added at all and if so it gets removed. This test +// passes iff both succeeds. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var notification = "http-on-modify-request"; + +var httpServer = null; + +var authCredentials = "guest:guest"; +var authPath = "/authTest"; +var authCredsURL = "http://" + authCredentials + "@localhost:8888" + authPath; +var authURL = "http://localhost:8888" + authPath; + +function authHandler(metadata, response) { + if (metadata.hasHeader("Test")) { + // Lets see if the auth header got deleted. + var noAuthHeader = false; + if (!metadata.hasHeader("Authorization")) { + noAuthHeader = true; + } + Assert.ok(noAuthHeader); + } + // Not our test request yet. + else if (!metadata.hasHeader("Authorization")) { + response.setStatusLine(metadata.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + } +} + +function RequestObserver() { + this.register(); +} + +RequestObserver.prototype = { + register() { + info("Registering " + notification); + Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService) + .addObserver(this, notification, true); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(subject, topic, data) { + if (topic == notification) { + if (!(subject instanceof Ci.nsIHttpChannel)) { + do_throw(notification + " observed a non-HTTP channel."); + } + try { + subject.getRequestHeader("Authorization"); + } catch (e) { + // Throw if there is no header to delete. We should get one iff caching + // the auth credentials is working and the header gets added _before_ + // "http-on-modify-request" gets called. + httpServer.stop(do_test_finished); + do_throw("No authorization header found, aborting!"); + } + // We are still here. Let's remove the authorization header now. + subject.setRequestHeader("Authorization", null, false); + } + }, +}; + +var listener = { + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + if (current_test < tests.length - 1) { + current_test++; + tests[current_test](); + } else { + do_test_pending(); + httpServer.stop(do_test_finished); + } + do_test_finished(); + }, +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var tests = [startAuthHeaderTest, removeAuthHeaderTest]; + +var current_test = 0; + +var requestObserver = null; + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler(authPath, authHandler); + httpServer.start(8888); + + tests[0](); +} + +function startAuthHeaderTest() { + var chan = makeChan(authCredsURL); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function removeAuthHeaderTest() { + // After caching the auth credentials in the first test, lets try to remove + // the authorization header now... + requestObserver = new RequestObserver(); + var chan = makeChan(authURL); + // Indicating that the request is coming from the second test. + chan.setRequestHeader("Test", "1", false); + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_bug894586.js b/netwerk/test/unit/test_bug894586.js new file mode 100644 index 0000000000..a3d1b78c29 --- /dev/null +++ b/netwerk/test/unit/test_bug894586.js @@ -0,0 +1,157 @@ +/* + * Tests for bug 894586: nsSyncLoadService::PushSyncStreamToListener + * should not fail for channels of unknown size + */ + +"use strict"; + +var contentSecManager = Cc["@mozilla.org/contentsecuritymanager;1"].getService( + Ci.nsIContentSecurityManager +); + +function ProtocolHandler() { + this.uri = Cc["@mozilla.org/network/simple-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec(this.scheme + ":dummy") + .finalize(); +} + +ProtocolHandler.prototype = { + /** nsIProtocolHandler */ + get scheme() { + return "x-bug894586"; + }, + get defaultPort() { + return -1; + }, + get protocolFlags() { + return ( + Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE | + Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE | + Ci.nsIProtocolHandler.URI_NON_PERSISTABLE | + Ci.nsIProtocolHandler.URI_SYNC_LOAD_IS_OK + ); + }, + newChannel(aURI, aLoadInfo) { + this.loadInfo = aLoadInfo; + return this; + }, + allowPort(port, scheme) { + return port != -1; + }, + + /** nsIChannel */ + get originalURI() { + return this.uri; + }, + get URI() { + return this.uri; + }, + owner: null, + notificationCallbacks: null, + get securityInfo() { + return null; + }, + get contentType() { + return "text/css"; + }, + set contentType(val) {}, + contentCharset: "UTF-8", + get contentLength() { + return -1; + }, + set contentLength(val) { + throw Components.Exception( + "Setting content length", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + }, + open() { + // throws an error if security checks fail + contentSecManager.performSecurityCheck(this, null); + + var file = do_get_file("test_bug894586.js", false); + Assert.ok(file.exists()); + var url = Services.io.newFileURI(file); + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).open(); + }, + asyncOpen(aListener, aContext) { + throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + contentDisposition: Ci.nsIChannel.DISPOSITION_INLINE, + get contentDispositionFilename() { + throw Components.Exception("No file name", Cr.NS_ERROR_NOT_AVAILABLE); + }, + get contentDispositionHeader() { + throw Components.Exception("No header", Cr.NS_ERROR_NOT_AVAILABLE); + }, + + /** nsIRequest */ + get name() { + return this.uri.spec; + }, + isPending: () => false, + get status() { + return Cr.NS_OK; + }, + cancel(status) {}, + loadGroup: null, + loadFlags: + Ci.nsIRequest.LOAD_NORMAL | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_BYPASS_CACHE, + + /** nsIFactory */ + createInstance(aOuter, aIID) { + if (aOuter) { + throw Components.Exception( + "createInstance no aggregation", + Cr.NS_ERROR_NO_AGGREGATION + ); + } + return this.QueryInterface(aIID); + }, + lockFactory() {}, + + /** nsISupports */ + QueryInterface: ChromeUtils.generateQI([ + "nsIProtocolHandler", + "nsIRequest", + "nsIChannel", + "nsIFactory", + ]), + classID: Components.ID("{16d594bc-d9d8-47ae-a139-ea714dc0c35c}"), +}; + +/** + * Attempt a sync load; we use the stylesheet service to do this for us, + * based on the knowledge that it forces a sync load under the hood. + */ +function run_test() { + var handler = new ProtocolHandler(); + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + handler.classID, + "", + "@mozilla.org/network/protocol;1?name=" + handler.scheme, + handler + ); + try { + var ss = Cc["@mozilla.org/content/style-sheet-service;1"].getService( + Ci.nsIStyleSheetService + ); + ss.loadAndRegisterSheet(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET); + Assert.ok( + ss.sheetRegistered(handler.uri, Ci.nsIStyleSheetService.AGENT_SHEET) + ); + } finally { + registrar.unregisterFactory(handler.classID, handler); + } +} + +// vim: set et ts=2 : diff --git a/netwerk/test/unit/test_bug935499.js b/netwerk/test/unit/test_bug935499.js new file mode 100644 index 0000000000..2da8168d2d --- /dev/null +++ b/netwerk/test/unit/test_bug935499.js @@ -0,0 +1,10 @@ +"use strict"; + +function run_test() { + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + var isASCII = {}; + Assert.equal(idnService.convertToDisplayIDN("xn--", isASCII), "xn--"); +} diff --git a/netwerk/test/unit/test_cache-control_request.js b/netwerk/test/unit/test_cache-control_request.js new file mode 100644 index 0000000000..501fa7ae87 --- /dev/null +++ b/netwerk/test/unit/test_cache-control_request.js @@ -0,0 +1,447 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +var cache = null; + +var base_url = "http://localhost:" + httpserver.identity.primaryPort; +var resource_age_100 = "/resource_age_100"; +var resource_age_100_url = base_url + resource_age_100; +var resource_stale_100 = "/resource_stale_100"; +var resource_stale_100_url = base_url + resource_stale_100; +var resource_fresh_100 = "/resource_fresh_100"; +var resource_fresh_100_url = base_url + resource_fresh_100; + +// Test flags +var hit_server = false; + +function make_channel(url, cache_control) { + // Reset test global status + hit_server = false; + + var req = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + req.QueryInterface(Ci.nsIHttpChannel); + if (cache_control) { + req.setRequestHeader("Cache-control", cache_control, false); + } + + return req; +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +function resource_age_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Age", "100", false); + response.setHeader("Last-Modified", date_string_from_now(-100), false); + response.setHeader("Expires", date_string_from_now(+9999), false); + + const body = "data1"; + response.bodyOutputStream.write(body, body.length); +} + +function resource_stale_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Date", date_string_from_now(-200), false); + response.setHeader("Last-Modified", date_string_from_now(-200), false); + response.setHeader("Cache-Control", "max-age=100", false); + response.setHeader("Expires", date_string_from_now(-100), false); + + const body = "data2"; + response.bodyOutputStream.write(body, body.length); +} + +function resource_fresh_100_handler(metadata, response) { + hit_server = true; + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Last-Modified", date_string_from_now(0), false); + response.setHeader("Cache-Control", "max-age=100", false); + response.setHeader("Expires", date_string_from_now(+100), false); + + const body = "data3"; + response.bodyOutputStream.write(body, body.length); +} + +function run_test() { + do_get_profile(); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserver.registerPathHandler(resource_age_100, resource_age_100_handler); + httpserver.registerPathHandler( + resource_stale_100, + resource_stale_100_handler + ); + httpserver.registerPathHandler( + resource_fresh_100, + resource_fresh_100_handler + ); + cache = getCacheStorage("disk"); + + wait_for_cache_index(run_next_test); +} + +// Here starts the list of tests + +// ============================================================================ +// Cache-Control: no-store + +add_test(() => { + // Must not create a cache entry + var ch = make_channel(resource_age_100_url, "no-store"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(!cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Prepare state only, cache the entry + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Try again, while we already keep a cache entry, + // the channel must not use it, entry should stay in the cache + var ch = make_channel(resource_age_100_url, "no-store"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: no-cache + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry should be revalidated (we expect a server hit) + var ch = make_channel(resource_age_100_url, "no-cache"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: max-age + +add_test(() => { + // Check the prepared cache entry is used when no special directives are added + var ch = make_channel(resource_age_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is greater than the maximum requested, + // should hit server + var ch = make_channel(resource_age_100_url, "max-age=10"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is greater than the maximum requested, + // but the max-stale directive says to use it when it's fresh enough + var ch = make_channel(resource_age_100_url, "max-age=10, max-stale=99999"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The existing entry's age is lesser than the maximum requested, + // should go from cache + var ch = make_channel(resource_age_100_url, "max-age=1000"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_age_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: max-stale + +add_test(() => { + // Preprate the entry first + var ch = make_channel(resource_stale_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + // Must shift the expiration time set on the entry to |now| be in the past + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // Check it's not reused (as it's stale) when no special directives + // are provided + var ch = make_channel(resource_stale_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // Accept cached responses of any stale time + var ch = make_channel(resource_stale_100_url, "max-stale"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // The entry is stale only by 100 seconds, accept it + var ch = make_channel(resource_stale_100_url, "max-stale=1000"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + do_timeout(1500, run_next_test); + }, null) + ); +}); + +add_test(() => { + // The entry is stale by 100 seconds but we only accept a 10 seconds stale + // entry, go from server + var ch = make_channel(resource_stale_100_url, "max-stale=10"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_stale_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Cache-Control: min-fresh + +add_test(() => { + // Preprate the entry first + var ch = make_channel(resource_fresh_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Check it's reused when no special directives are provided + var ch = make_channel(resource_fresh_100_url); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // Entry fresh enough to be served from the cache + var ch = make_channel(resource_fresh_100_url, "min-fresh=10"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(!hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + // The entry is not fresh enough + var ch = make_channel(resource_fresh_100_url, "min-fresh=1000"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Parser test, if the Cache-Control header would not parse correctly, the entry +// doesn't load from the server. + +add_test(() => { + var ch = make_channel( + resource_fresh_100_url, + 'unknown1,unknown2 = "a,b", min-fresh = 1000 ' + ); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +add_test(() => { + var ch = make_channel(resource_fresh_100_url, "no-cache = , min-fresh = 10"); + ch.asyncOpen( + new ChannelListener(function(request, data) { + Assert.ok(hit_server); + Assert.ok(cache.exists(make_uri(resource_fresh_100_url), "")); + + run_next_test(); + }, null) + ); +}); + +// ============================================================================ +// Done + +add_test(() => { + run_next_test(); + httpserver.stop(do_test_finished); +}); + +// ============================================================================ +// Helpers + +function date_string_from_now(delta_secs) { + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + var d = new Date(); + d.setTime(d.getTime() + delta_secs * 1000); + return ( + days[d.getUTCDay()] + + ", " + + d.getUTCDate() + + " " + + months[d.getUTCMonth()] + + " " + + d.getUTCFullYear() + + " " + + d.getUTCHours() + + ":" + + d.getUTCMinutes() + + ":" + + d.getUTCSeconds() + + " UTC" + ); +} diff --git a/netwerk/test/unit/test_cache-entry-id.js b/netwerk/test/unit/test_cache-entry-id.js new file mode 100644 index 0000000000..121c30ced4 --- /dev/null +++ b/netwerk/test/unit/test_cache-entry-id.js @@ -0,0 +1,215 @@ +/** + * Test for the "CacheEntryId" under several cases. + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort + "/content"; +}); + +var httpServer = null; + +const responseContent = "response body"; +const responseContent2 = "response body 2"; +const altContent = "!@#$%^&*()"; +const altContentType = "text/binary"; + +function isParentProcess() { + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + return ( + !appInfo || + appInfo.getService(Ci.nsIXULRuntime).processType == + Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +var handlers = [ + (m, r) => { + r.bodyOutputStream.write(responseContent, responseContent.length); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, + (m, r) => { + r.bodyOutputStream.write(responseContent2, responseContent2.length); + }, + (m, r) => { + r.setStatusLine(m.httpVersion, 304, "Not Modified"); + }, +]; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-cache"); + + var handler = handlers.shift(); + if (handler) { + handler(metadata, response); + return; + } + + Assert.ok(false, "Should not reach here."); +} + +function fetch(preferredDataType = null) { + return new Promise(resolve => { + var chan = NetUtil.newChannel({ uri: URL, loadUsingSystemPrincipal: true }); + + if (preferredDataType) { + var cc = chan.QueryInterface(Ci.nsICacheInfoChannel); + cc.preferAlternativeDataType(altContentType, "", true); + } + + chan.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache, cacheEntryId) => { + resolve({ request, buffer, isFromCache, cacheEntryId }); + }, null) + ); + }); +} + +function check( + response, + content, + preferredDataType, + isFromCache, + cacheEntryIdChecker +) { + var cc = response.request.QueryInterface(Ci.nsICacheInfoChannel); + + Assert.equal(response.buffer, content); + Assert.equal(cc.alternativeDataType, preferredDataType); + Assert.equal(response.isFromCache, isFromCache); + Assert.ok(!cacheEntryIdChecker || cacheEntryIdChecker(response.cacheEntryId)); + + return response; +} + +function writeAltData(request) { + var cc = request.QueryInterface(Ci.nsICacheInfoChannel); + var os = cc.openAlternativeOutputStream(altContentType, altContent.length); + os.write(altContent, altContent.length); + os.close(); + gc(); // We need to do a GC pass to ensure the cache entry has been freed. + + return new Promise(resolve => { + if (isParentProcess()) { + Services.cache2.QueryInterface(Ci.nsICacheTesting).flush(resolve); + } else { + do_send_remote_message("flush"); + do_await_remote_message("flushed").then(resolve); + } + }); +} + +function run_test() { + do_get_profile(); + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + do_test_pending(); + + var targetCacheEntryId = null; + + return ( + Promise.resolve() + // Setup testing environment: Placing alternative data into HTTP cache. + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + responseContent, + "", + false, + cacheEntryId => cacheEntryId === undefined + ) + ) + .then(r => writeAltData(r.request)) + + // Start testing. + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + altContent, + altContentType, + true, + cacheEntryId => cacheEntryId !== undefined + ) + ) + .then(r => (targetCacheEntryId = r.cacheEntryId)) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent, + "", + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch(altContentType)) + .then(r => + check( + r, + altContent, + altContentType, + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent, + "", + true, + cacheEntryId => cacheEntryId === targetCacheEntryId + ) + ) + + .then(_ => fetch()) // The response is changed here. + .then(r => + check( + r, + responseContent2, + "", + false, + cacheEntryId => cacheEntryId === undefined + ) + ) + + .then(_ => fetch()) + .then(r => + check( + r, + responseContent2, + "", + true, + cacheEntryId => + cacheEntryId !== undefined && cacheEntryId !== targetCacheEntryId + ) + ) + + // Tear down. + .catch(e => Assert.ok(false, "Unexpected exception: " + e)) + .then(_ => Assert.equal(handlers.length, 0)) + .then(_ => httpServer.stop(do_test_finished)) + ); +} diff --git a/netwerk/test/unit/test_cache2-00-service-get.js b/netwerk/test/unit/test_cache2-00-service-get.js new file mode 100644 index 0000000000..7286041111 --- /dev/null +++ b/netwerk/test/unit/test_cache2-00-service-get.js @@ -0,0 +1,19 @@ +"use strict"; + +function run_test() { + // Just check the contract ID alias works well. + try { + var serviceA = Cc[ + "@mozilla.org/netwerk/cache-storage-service;1" + ].getService(Ci.nsICacheStorageService); + Assert.ok(serviceA); + var serviceB = Cc[ + "@mozilla.org/network/cache-storage-service;1" + ].getService(Ci.nsICacheStorageService); + Assert.ok(serviceB); + + Assert.equal(serviceA, serviceB); + } catch (ex) { + do_throw("Cannot instantiate cache storage service: " + ex); + } +} diff --git a/netwerk/test/unit/test_cache2-01-basic.js b/netwerk/test/unit/test_cache2-01-basic.js new file mode 100644 index 0000000000..ee7da95c17 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01-basic.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01a-basic-readonly.js b/netwerk/test/unit/test_cache2-01a-basic-readonly.js new file mode 100644 index 0000000000..32bed8a69e --- /dev/null +++ b/netwerk/test/unit/test_cache2-01a-basic-readonly.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://ro/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01b-basic-datasize.js b/netwerk/test/unit/test_cache2-01b-basic-datasize.js new file mode 100644 index 0000000000..9c41e114c3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01b-basic-datasize.js @@ -0,0 +1,49 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) { + // Open for read and check + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Open for rewrite (truncate), write different meta and data + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW | WAITFORWRITE, "a2m", "a2d", function(entry) { + // Open for read and check + Assert.equal(entry.dataSize, 3); + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function(entry) { + Assert.equal(entry.dataSize, 3); + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js new file mode 100644 index 0000000000..7c97c5ced5 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01c-basic-hasmeta-only.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | METAONLY, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "", function(entry) { + // Open for rewrite (truncate), write different meta and data + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + new OpenCallback(NEW, "a2m", "a2d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://mt/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a2m", "a2d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js new file mode 100644 index 0000000000..faed1c9d69 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01d-basic-not-wanted.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Open but don't want the entry + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NOTWANTED, "a1m", "a1d", function(entry) { + // Open for read again and check the entry is OK + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js new file mode 100644 index 0000000000..a151f2e752 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01e-basic-bypass-if-busy.js @@ -0,0 +1,39 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, delay the actual write + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DONTFILL, "a1m", "a1d", function(entry) { + var bypassed = false; + + // Open and bypass + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_BYPASS_IF_BUSY, + null, + new OpenCallback(NOTFOUND, "", "", function(entry) { + Assert.ok(!bypassed); + bypassed = true; + }) + ); + + // do_execute_soon for two reasons: + // 1. we want finish_cache2_test call for sure after do_test_pending, but all the callbacks here + // may invoke synchronously + // 2. precaution when the OPEN_BYPASS_IF_BUSY invocation become a post one day + executeSoon(function() { + Assert.ok(bypassed); + finish_cache2_test(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js new file mode 100644 index 0000000000..2ee2bf85b8 --- /dev/null +++ b/netwerk/test/unit/test_cache2-01f-basic-openTruncate.js @@ -0,0 +1,24 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + var entry = storage.openTruncate(createURI("http://new1/"), ""); + Assert.ok(!!entry); + + // Fill the entry, and when done, check it's content + new OpenCallback(NEW, "meta", "data", function() { + asyncOpenCacheEntry( + "http://new1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "meta", "data", function() { + finish_cache2_test(); + }) + ); + }).onCacheEntryAvailable(entry, true, null, 0); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-02-open-non-existing.js b/netwerk/test/unit/test_cache2-02-open-non-existing.js new file mode 100644 index 0000000000..8c00e2f632 --- /dev/null +++ b/netwerk/test/unit/test_cache2-02-open-non-existing.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open non-existing for read, should fail + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function(entry) { + // Open the same non-existing for read again, should fail second time + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + new OpenCallback(NOTFOUND, null, null, function(entry) { + // Try it again normally, should go + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function(entry) { + // ...and check + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js new file mode 100644 index 0000000000..f506b1165e --- /dev/null +++ b/netwerk/test/unit/test_cache2-03-oncacheentryavail-throws.js @@ -0,0 +1,36 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open but let OCEA throw + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) { + // Try it again, should go + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "c1m", "c1d", function(entry) { + // ...and check + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(false, "c1m", "c1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js new file mode 100644 index 0000000000..164ec4c1ed --- /dev/null +++ b/netwerk/test/unit/test_cache2-04-oncacheentryavail-throws2x.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open but let OCEA throw + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) { + // Open but let OCEA throw ones again + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | THROWAVAIL, null, null, function(entry) { + // Try it again, should go + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "d1m", "d1d", function(entry) { + // ...and check + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "d1m", "d1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-05-visit.js b/netwerk/test/unit/test_cache2-05-visit.js new file mode 100644 index 0000000000..ecf3f8061e --- /dev/null +++ b/netwerk/test/unit/test_cache2-05-visit.js @@ -0,0 +1,113 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + var mc = new MultipleCallbacks(4, function() { + // Method asyncVisitStorage() gets the data from index on Cache I/O thread + // with INDEX priority, so it is ensured that index contains information + // about all pending writes. However, OpenCallback emulates network latency + // by postponing the writes using do_execute_soon. We must do the same here + // to make sure that all writes are posted to Cache I/O thread before we + // visit the storage. + executeSoon(function() { + syncWithCacheIOThread(function() { + var expectedConsumption = 4096; + + storage.asyncVisitStorage( + // Test should store 4 entries + new VisitCallback( + 4, + expectedConsumption, + ["http://a/", "http://b/", "http://c/", "http://d/"], + function() { + storage.asyncVisitStorage( + // Still 4 entries expected, now don't walk them + new VisitCallback(4, expectedConsumption, null, function() { + finish_cache2_test(); + }), + false + ); + } + ), + true + ); + }); + }); + }); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "c1m", "c1d", function(entry) { + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "c1m", "c1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "d1m", "d1d", function(entry) { + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "d1m", "d1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-06-pb-mode.js b/netwerk/test/unit/test_cache2-06-pb-mode.js new file mode 100644 index 0000000000..1eebb84543 --- /dev/null +++ b/netwerk/test/unit/test_cache2-06-pb-mode.js @@ -0,0 +1,53 @@ +"use strict"; + +function exitPB() { + var obsvc = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + obsvc.notifyObservers(null, "last-pb-context-exited"); +} + +function run_test() { + do_get_profile(); + + // Store PB entry + asyncOpenCacheEntry( + "http://p1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.private, + new OpenCallback(NEW, "p1m", "p1d", function(entry) { + asyncOpenCacheEntry( + "http://p1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.private, + new OpenCallback(NORMAL, "p1m", "p1d", function(entry) { + // Check it's there + syncWithCacheIOThread(function() { + var storage = getCacheStorage( + "disk", + Services.loadContextInfo.private + ); + storage.asyncVisitStorage( + new VisitCallback(1, 12, ["http://p1/"], function() { + // Simulate PB exit + exitPB(); + // Check the entry is gone + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-07-visit-memory.js b/netwerk/test/unit/test_cache2-07-visit-memory.js new file mode 100644 index 0000000000..79065cab19 --- /dev/null +++ b/netwerk/test/unit/test_cache2-07-visit-memory.js @@ -0,0 +1,123 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Add entry to the memory storage + var mc = new MultipleCallbacks(5, function() { + // Check it's there by visiting the storage + syncWithCacheIOThread(function() { + var storage = getCacheStorage("memory"); + storage.asyncVisitStorage( + new VisitCallback(1, 12, ["http://mem1/"], function() { + storage = getCacheStorage("disk"); + storage.asyncVisitStorage( + // Previous tests should store 4 disk entries + new VisitCallback( + 4, + 4096, + ["http://a/", "http://b/", "http://c/", "http://d/"], + function() { + finish_cache2_test(); + } + ), + true + ); + }), + true + ); + }); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m1m", "m1d", function(entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m1m", "m1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://c/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://d/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-07a-open-memory.js b/netwerk/test/unit/test_cache2-07a-open-memory.js new file mode 100644 index 0000000000..6944064697 --- /dev/null +++ b/netwerk/test/unit/test_cache2-07a-open-memory.js @@ -0,0 +1,81 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // First check how behaves the memory storage. + + asyncOpenCacheEntry( + "http://mem-first/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "mem1-meta", "mem1-data", function(entryM1) { + Assert.ok(!entryM1.persistent); + asyncOpenCacheEntry( + "http://mem-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "mem1-meta", "mem1-data", function(entryM2) { + Assert.ok(!entryM1.persistent); + Assert.ok(!entryM2.persistent); + + // Now check the disk storage behavior. + + asyncOpenCacheEntry( + "http://disk-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + // Must wait for write, since opening the entry as memory-only before the disk one + // is written would cause NS_ERROR_NOT_AVAILABLE from openOutputStream when writing + // this disk entry since it's doomed during opening of the memory-only entry for the same URL. + new OpenCallback( + NEW | WAITFORWRITE, + "disk1-meta", + "disk1-data", + function(entryD1) { + Assert.ok(entryD1.persistent); + // Now open the same URL as a memory-only entry, the disk entry must be doomed. + asyncOpenCacheEntry( + "http://disk-first/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + // This must be recreated + new OpenCallback(NEW, "mem2-meta", "mem2-data", function( + entryD2 + ) { + Assert.ok(entryD1.persistent); + Assert.ok(!entryD2.persistent); + // Check we get it back, even when opening via the disk storage + asyncOpenCacheEntry( + "http://disk-first/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NORMAL, + "mem2-meta", + "mem2-data", + function(entryD3) { + Assert.ok(entryD1.persistent); + Assert.ok(!entryD2.persistent); + Assert.ok(!entryD3.persistent); + finish_cache2_test(); + } + ) + ); + }) + ); + } + ) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js new file mode 100644 index 0000000000..7e62c45030 --- /dev/null +++ b/netwerk/test/unit/test_cache2-08-evict-disk-by-memory-storage.js @@ -0,0 +1,25 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + var storage = getCacheStorage("memory"); + // Have to fail + storage.asyncDoomURI( + createURI("http://a/"), + "", + new EvictionCallback(false, function() { + finish_cache2_test(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js new file mode 100644 index 0000000000..dd444f82fb --- /dev/null +++ b/netwerk/test/unit/test_cache2-09-evict-disk-by-uri.js @@ -0,0 +1,32 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + var storage = getCacheStorage("disk"); + storage.asyncDoomURI( + createURI("http://a/"), + "", + new EvictionCallback(true, function() { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-10-evict-direct.js b/netwerk/test/unit/test_cache2-10-evict-direct.js new file mode 100644 index 0000000000..f9f32a9ab2 --- /dev/null +++ b/netwerk/test/unit/test_cache2-10-evict-direct.js @@ -0,0 +1,29 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function(entry) { + entry.asyncDoom( + new EvictionCallback(true, function() { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js new file mode 100644 index 0000000000..688b1f1e00 --- /dev/null +++ b/netwerk/test/unit/test_cache2-10b-evict-direct-immediate.js @@ -0,0 +1,21 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DOOMED, "b1m", "b1d", function(entry) { + entry.asyncDoom( + new EvictionCallback(true, function() { + finish_cache2_test(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-11-evict-memory.js b/netwerk/test/unit/test_cache2-11-evict-memory.js new file mode 100644 index 0000000000..5868a29552 --- /dev/null +++ b/netwerk/test/unit/test_cache2-11-evict-memory.js @@ -0,0 +1,89 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("memory"); + var mc = new MultipleCallbacks(3, function() { + storage.asyncEvictStorage( + new EvictionCallback(true, function() { + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function() { + var storage = getCacheStorage("disk"); + + var expectedConsumption = 2048; + + storage.asyncVisitStorage( + new VisitCallback( + 2, + expectedConsumption, + ["http://a/", "http://b/"], + function() { + finish_cache2_test(); + } + ), + true + ); + }), + true + ); + }) + ); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m2m", "m2d", function(entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m2m", "m2d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-12-evict-disk.js b/netwerk/test/unit/test_cache2-12-evict-disk.js new file mode 100644 index 0000000000..5c40ba94bf --- /dev/null +++ b/netwerk/test/unit/test_cache2-12-evict-disk.js @@ -0,0 +1,81 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(3, function() { + var storage = getCacheStorage("disk"); + storage.asyncEvictStorage( + new EvictionCallback(true, function() { + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function() { + var storage = getCacheStorage("memory"); + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }) + ); + }); + + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m2m", "m2d", function(entry) { + asyncOpenCacheEntry( + "http://mem1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m2m", "m2d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "b1m", "b1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "b1m", "b1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-13-evict-non-existing.js b/netwerk/test/unit/test_cache2-13-evict-non-existing.js new file mode 100644 index 0000000000..9c25a48d7b --- /dev/null +++ b/netwerk/test/unit/test_cache2-13-evict-non-existing.js @@ -0,0 +1,16 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var storage = getCacheStorage("disk"); + storage.asyncDoomURI( + createURI("http://non-existing/"), + "", + new EvictionCallback(false, function() { + finish_cache2_test(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-14-concurent-readers.js b/netwerk/test/unit/test_cache2-14-concurent-readers.js new file mode 100644 index 0000000000..03aded21b0 --- /dev/null +++ b/netwerk/test/unit/test_cache2-14-concurent-readers.js @@ -0,0 +1,48 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "x1m", "x1d", function(entry) { + // nothing to do here, we expect concurent callbacks to get + // all notified, then the test finishes + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "x1m", "x1d", function(entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js new file mode 100644 index 0000000000..cb00a4a8cd --- /dev/null +++ b/netwerk/test/unit/test_cache2-14b-concurent-readers-complete.js @@ -0,0 +1,76 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "x1m", "x1d", function(entry) { + // nothing to do here, we expect concurent callbacks to get + // all notified, then the test finishes + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + var order = 0; + + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NORMAL | COMPLETE | NOTIFYBEFOREREAD, + "x1m", + "x1d", + function(entry, beforeReading) { + if (beforeReading) { + ++order; + Assert.equal(order, 3); + } else { + mc.fired(); + } + } + ) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function( + entry, + beforeReading + ) { + if (beforeReading) { + ++order; + Assert.equal(order, 1); + } else { + mc.fired(); + } + }) + ); + asyncOpenCacheEntry( + "http://x/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL | NOTIFYBEFOREREAD, "x1m", "x1d", function( + entry, + beforeReading + ) { + if (beforeReading) { + ++order; + Assert.equal(order, 2); + } else { + mc.fired(); + } + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-15-conditional-304.js b/netwerk/test/unit/test_cache2-15-conditional-304.js new file mode 100644 index 0000000000..b7271996b9 --- /dev/null +++ b/netwerk/test/unit/test_cache2-15-conditional-304.js @@ -0,0 +1,60 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "31m", "31d", function(entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(REVAL, "31m", "31d", function(entry) { + // emulate 304 from the server + executeSoon(function() { + entry.setValid(); // this will trigger OpenCallbacks bellow + }); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://304/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "31m", "31d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-16-conditional-200.js b/netwerk/test/unit/test_cache2-16-conditional-200.js new file mode 100644 index 0000000000..60b38cd2e1 --- /dev/null +++ b/netwerk/test/unit/test_cache2-16-conditional-200.js @@ -0,0 +1,76 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "21m", "21d", function(entry) { + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "21m", "21d", function(entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(REVAL, "21m", "21d", function(entry) { + // emulate 200 from server (new content) + executeSoon(function() { + var entry2 = entry.recreate(); + + // now fill the new entry, use OpenCallback directly for it + new OpenCallback( + NEW, + "22m", + "22d", + function() {} + ).onCacheEntryAvailable(entry2, true, null, Cr.NS_OK); + }); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "22m", "22d", function(entry) { + mc.fired(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-17-evict-all.js b/netwerk/test/unit/test_cache2-17-evict-all.js new file mode 100644 index 0000000000..48ca1c15ef --- /dev/null +++ b/netwerk/test/unit/test_cache2-17-evict-all.js @@ -0,0 +1,18 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var svc = get_cache_service(); + svc.clear(); + + var storage = getCacheStorage("disk"); + storage.asyncVisitStorage( + new VisitCallback(0, 0, [], function() { + finish_cache2_test(); + }), + true + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-18-not-valid.js b/netwerk/test/unit/test_cache2-18-not-valid.js new file mode 100644 index 0000000000..82bafb5ea9 --- /dev/null +++ b/netwerk/test/unit/test_cache2-18-not-valid.js @@ -0,0 +1,38 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write but expect it to fail, since other callback will recreate (and doom) + // the first entry before it opens output stream (note: in case of problems the DOOMED flag + // can be removed, it is not the test failure when opening the output stream on recreated entry. + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW | DOOMED, "v1m", "v1d", function(entry) { + // Open for rewrite (don't validate), write different meta and data + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NOTVALID | RECREATE, "v2m", "v2d", function(entry) { + // And check... + asyncOpenCacheEntry( + "http://nv/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "v2m", "v2d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-19-range-206.js b/netwerk/test/unit/test_cache2-19-range-206.js new file mode 100644 index 0000000000..1baddebbd6 --- /dev/null +++ b/netwerk/test/unit/test_cache2-19-range-206.js @@ -0,0 +1,65 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "206m", "206part1-", function(entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(PARTIAL, "206m", "206part1-", function(entry) { + // emulate 206 from the server, i.e. resume transaction and write content to the output stream + new OpenCallback( + NEW | WAITFORWRITE | PARTIAL, + "206m", + "-part2", + function(entry) { + entry.setValid(); + } + ).onCacheEntryAvailable(entry, true, null, Cr.NS_OK); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r206/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "206m", "206part1--part2", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-20-range-200.js b/netwerk/test/unit/test_cache2-20-range-200.js new file mode 100644 index 0000000000..01ed41d2e7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-20-range-200.js @@ -0,0 +1,66 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "200m1", "200part1a-", function(entry) { + // Open normally but wait for validation from the server + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(PARTIAL, "200m1", "200part1a-", function(entry) { + // emulate 200 from the server, i.e. recreate the entry, resume transaction and + // write new content to the output stream + new OpenCallback( + NEW | WAITFORWRITE | RECREATE, + "200m2", + "200part1b--part2b", + function(entry) { + entry.setValid(); + } + ).onCacheEntryAvailable(entry, true, null, Cr.NS_OK); + }) + ); + + var mc = new MultipleCallbacks(3, finish_cache2_test); + + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) { + mc.fired(); + }) + ); + asyncOpenCacheEntry( + "http://r200/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "200m2", "200part1b--part2b", function(entry) { + mc.fired(); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-21-anon-storage.js b/netwerk/test/unit/test_cache2-21-anon-storage.js new file mode 100644 index 0000000000..3e2b195d5f --- /dev/null +++ b/netwerk/test/unit/test_cache2-21-anon-storage.js @@ -0,0 +1,52 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Create and check an entry anon disk storage + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NEW, "an1", "an1", function(entry) { + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NORMAL, "an1", "an1", function(entry) { + // Create and check an entry non-anon disk storage + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW, "na1", "na1", function(entry) { + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "na1", "na1", function(entry) { + // check the anon entry is still there and intact + asyncOpenCacheEntry( + "http://anon1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NORMAL, "an1", "an1", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-22-anon-visit.js b/netwerk/test/unit/test_cache2-22-anon-visit.js new file mode 100644 index 0000000000..b9bc961cfa --- /dev/null +++ b/netwerk/test/unit/test_cache2-22-anon-visit.js @@ -0,0 +1,43 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function() { + var storage = getCacheStorage("disk", Services.loadContextInfo.default); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function() { + storage = getCacheStorage("disk", Services.loadContextInfo.anonymous); + storage.asyncVisitStorage( + new VisitCallback(1, 1024, ["http://an2/"], function() { + finish_cache2_test(); + }), + true + ); + }), + true + ); + }); + + asyncOpenCacheEntry( + "http://an2/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function(entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry( + "http://an2/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.anonymous, + new OpenCallback(NEW | WAITFORWRITE, "an2", "an2", function(entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-23-read-over-chunk.js b/netwerk/test/unit/test_cache2-23-read-over-chunk.js new file mode 100644 index 0000000000..81871224a7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-23-read-over-chunk.js @@ -0,0 +1,34 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + const kChunkSize = 256 * 1024; + + var payload = ""; + for (var i = 0; i < kChunkSize + 10; ++i) { + if (i < kChunkSize - 5) { + payload += "0"; + } else { + payload += String.fromCharCode(i + 65); + } + } + + asyncOpenCacheEntry( + "http://read/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "", payload, function(entry) { + var is = entry.openInputStream(0); + pumpReadStream(is, function(read) { + Assert.equal(read.length, kChunkSize + 10); + is.close(); + Assert.ok(read == payload); // not using do_check_eq since logger will fail for the 1/4MB string + finish_cache2_test(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-24-exists.js b/netwerk/test/unit/test_cache2-24-exists.js new file mode 100644 index 0000000000..67582594cc --- /dev/null +++ b/netwerk/test/unit/test_cache2-24-exists.js @@ -0,0 +1,43 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function() { + var mem = getCacheStorage("memory"); + var disk = getCacheStorage("disk"); + + Assert.ok(disk.exists(createURI("http://m1/"), "")); + Assert.ok(mem.exists(createURI("http://m1/"), "")); + Assert.ok(!mem.exists(createURI("http://m2/"), "")); + Assert.ok(disk.exists(createURI("http://d1/"), "")); + do_check_throws_nsIException( + () => disk.exists(createURI("http://d2/"), ""), + "NS_ERROR_NOT_AVAILABLE" + ); + + finish_cache2_test(); + }); + + asyncOpenCacheEntry( + "http://d1/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function(entry) { + mc.fired(); + }) + ); + + asyncOpenCacheEntry( + "http://m1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "meta", "data", function(entry) { + mc.fired(); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js new file mode 100644 index 0000000000..c1c82c4141 --- /dev/null +++ b/netwerk/test/unit/test_cache2-25-chunk-memory-limit.js @@ -0,0 +1,57 @@ +"use strict"; + +function gen_200k() { + var i; + var data = "0123456789ABCDEFGHIJLKMNO"; + for (i = 0; i < 13; i++) { + data += data; + } + return data; +} + +// Keep the output stream of the first entry in a global variable, so the +// CacheFile and its buffer isn't released before we write the data to the +// second entry. +var oStr; + +function run_test() { + do_get_profile(); + + var prefBranch = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + + // set max chunks memory so that only one full chunk fits within the limit + prefBranch.setIntPref("browser.cache.disk.max_chunks_memory_usage", 300); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_OK); + var data = gen_200k(); + oStr = entry.openOutputStream(0, data.length); + Assert.equal(data.length, oStr.write(data, data.length)); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + function(status, entry) { + Assert.equal(status, Cr.NS_OK); + var oStr2 = entry.openOutputStream(0, data.length); + do_check_throws_nsIException( + () => oStr2.write(data, data.length), + "NS_ERROR_OUT_OF_MEMORY" + ); + finish_cache2_test(); + } + ); + } + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-26-no-outputstream-open.js b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js new file mode 100644 index 0000000000..378e41b5db --- /dev/null +++ b/netwerk/test/unit/test_cache2-26-no-outputstream-open.js @@ -0,0 +1,36 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, but never write and never mark valid + asyncOpenCacheEntry( + "http://no-data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback( + NEW | METAONLY | DONTSETVALID | WAITFORWRITE, + "meta", + "", + function(entry) { + // Open again, we must get the callback and zero-length data + executeSoon(() => { + Cu.forceGC(); // invokes OnHandleClosed on the entry + + asyncOpenCacheEntry( + "http://no-data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "meta", "", function(entry) { + finish_cache2_test(); + }) + ); + }); + } + ) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-27-force-valid-for.js b/netwerk/test/unit/test_cache2-27-force-valid-for.js new file mode 100644 index 0000000000..223fbd6057 --- /dev/null +++ b/netwerk/test/unit/test_cache2-27-force-valid-for.js @@ -0,0 +1,35 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var mc = new MultipleCallbacks(2, function() { + finish_cache2_test(); + }); + + asyncOpenCacheEntry( + "http://m1/", + "memory", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NEW, "meta", "data", function(entry) { + // Check the default + equal(entry.isForcedValid, false); + + // Forced valid and confirm + entry.forceValidFor(2); + do_timeout(1000, function() { + equal(entry.isForcedValid, true); + mc.fired(); + }); + + // Confirm the timeout occurs + do_timeout(3000, function() { + equal(entry.isForcedValid, false); + mc.fired(); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-28-last-access-attrs.js b/netwerk/test/unit/test_cache2-28-last-access-attrs.js new file mode 100644 index 0000000000..50c6b99938 --- /dev/null +++ b/netwerk/test/unit/test_cache2-28-last-access-attrs.js @@ -0,0 +1,46 @@ +"use strict"; + +function run_test() { + do_get_profile(); + function NowSeconds() { + return parseInt(new Date().getTime() / 1000); + } + function do_check_time(t, min, max) { + Assert.ok(t >= min); + Assert.ok(t <= max); + } + + var timeStart = NowSeconds(); + + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m", "d", function(entry) { + var firstOpen = NowSeconds(); + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, timeStart, firstOpen); + do_check_time(entry.lastModified, timeStart, firstOpen); + + do_timeout(2000, () => { + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "m", "d", function(entry) { + var secondOpen = NowSeconds(); + Assert.equal(entry.fetchCount, 2); + do_check_time(entry.lastFetched, firstOpen, secondOpen); + do_check_time(entry.lastModified, timeStart, firstOpen); + + finish_cache2_test(); + }) + ); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js new file mode 100644 index 0000000000..7ed078786a --- /dev/null +++ b/netwerk/test/unit/test_cache2-28a-OPEN_SECRETLY.js @@ -0,0 +1,42 @@ +"use strict"; + +function run_test() { + do_get_profile(); + function NowSeconds() { + return parseInt(new Date().getTime() / 1000); + } + function do_check_time(a, b) { + Assert.ok(Math.abs(a - b) < 0.5); + } + + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "m", "d", function(entry) { + var now1 = NowSeconds(); + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, now1); + do_check_time(entry.lastModified, now1); + + do_timeout(2000, () => { + asyncOpenCacheEntry( + "http://t/", + "disk", + Ci.nsICacheStorage.OPEN_SECRETLY, + null, + new OpenCallback(NORMAL, "m", "d", function(entry) { + Assert.equal(entry.fetchCount, 1); + do_check_time(entry.lastFetched, now1); + do_check_time(entry.lastModified, now1); + + finish_cache2_test(); + }) + ); + }); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js new file mode 100644 index 0000000000..a686aaa046 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29a-concurrent_read_resumable_entry_size_zero.js @@ -0,0 +1,74 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits +This test is using a resumable response. +- with a profile, set max-entry-size to 0 +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry, it's doomed +- second channel now must engage interrupted concurrent write algorithm and read the content again from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "response body"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Range", "0-12/13"); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen(new ChannelListener(secondTimeThrough, null)); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js new file mode 100644 index 0000000000..e0da2202c3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29b-concurrent_read_non-resumable_entry_size_zero.js @@ -0,0 +1,78 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This test is using a non-resumable response. +- with a profile, set max-entry-size to 0 +- first channel makes a request for a non-resumable (chunked) response +- second channel makes a request for the same resource, concurrent read is bypassed (non-resumable response) +- first channel writes first bytes to the cache output stream, but that fails because of the max-entry-size limit and entry is doomed +- cache entry output stream is closed +- second channel gets the entry, opening the input stream must fail +- second channel must read the content again from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +const responseBody = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n"; +const responseBodyDecoded = "data reachedhej"; + +function contentHandler(metadata, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(responseBody); + response.finish(); +} + +function run_test() { + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 0); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen( + new ChannelListener(firstTimeThrough, null, CL_ALLOW_UNKNOWN_CL) + ); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_ALLOW_UNKNOWN_CL) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBodyDecoded); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBodyDecoded); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js new file mode 100644 index 0000000000..b76db5c39b --- /dev/null +++ b/netwerk/test/unit/test_cache2-29c-concurrent_read_half-interrupted.js @@ -0,0 +1,93 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29a test, this test checks that cocurrency is resumed when the first channel is interrupted +in the middle of reading and the second channel already consumed some content from the cache entry. +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- both channels must deliver full content w/o errors + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + + let len = responseBody.length; + response.setHeader("Content-Range", "0-" + (len - 1) + "/" + len); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen(new ChannelListener(secondTimeThrough, null)); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js new file mode 100644 index 0000000000..fe64d93007 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29d-concurrent_read_half-corrupted-206.js @@ -0,0 +1,93 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is broken (bad Content-Range header) +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + if (metadata.hasHeader("If-Range")) { + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + // Deliberately broken response header to trigger corrupted content error on the second channel + response.setHeader("Content-Range", "0-1/2"); + } + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js new file mode 100644 index 0000000000..45e67e63a3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-29e-concurrent_read_half-non-206-response.js @@ -0,0 +1,88 @@ +/* + +Checkes if the concurrent cache read/write works when the write is interrupted because of max-entry-size limits. +This is enhancement of 29c test, this test checks that a corrupted 206 response is correctly handled (no crashes or asserion failures) +This test is using a resumable response. +- with a profile, set max-entry-size to 1 (=1024 bytes) +- first channel makes a request for a resumable response +- second channel makes a request for the same resource, concurrent read happens +- first channel sets predicted data size on the entry with every chunk, it's doomed on 1024 +- second channel now must engage interrupted concurrent write algorithm and read the rest of the content from the network +- the response to the range request is plain 200 +- the first must deliver full content w/o errors +- the second channel must correctly fail + +*/ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpServer.identity.primaryPort; +}); + +var httpServer = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +// need something bigger than 1024 bytes +const responseBody = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=99999"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + responseBody.length); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + // Static check + Assert.ok(responseBody.length > 1024); + + do_get_profile(); + + Services.prefs.setIntPref("browser.cache.disk.max_entry_size", 1); + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpServer = new HttpServer(); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var chan1 = make_channel(URL + "/content"); + chan1.asyncOpen(new ChannelListener(firstTimeThrough, null)); + var chan2 = make_channel(URL + "/content"); + chan2.asyncOpen( + new ChannelListener(secondTimeThrough, null, CL_EXPECT_FAILURE) + ); + }); + + do_test_pending(); +} + +function firstTimeThrough(request, buffer) { + Assert.equal(buffer, responseBody); +} + +function secondTimeThrough(request, buffer) { + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache2-30a-entry-pinning.js b/netwerk/test/unit/test_cache2-30a-entry-pinning.js new file mode 100644 index 0000000000..f5ef4e69f3 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30a-entry-pinning.js @@ -0,0 +1,39 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + // Open for write, write + asyncOpenCacheEntry( + "http://a/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + Services.loadContextInfo.default, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) { + // Open for read and check + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Now clear the whole cache + get_cache_service().clear(); + + // The pinned entry should be intact + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + Services.loadContextInfo.default, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js new file mode 100644 index 0000000000..68dcd995cc --- /dev/null +++ b/netwerk/test/unit/test_cache2-30b-pinning-storage-clear.js @@ -0,0 +1,45 @@ +"use strict"; + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + + // Open a pinned entry for write, write + asyncOpenCacheEntry( + "http://a/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "a1m", "a1d", function(entry) { + // Now clear the disk storage, that should leave the pinned entry in the cache + var diskStorage = getCacheStorage("disk", lci); + diskStorage.asyncEvictStorage(null); + + // Open for read and check, it should still be there + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + // Now clear the pinning storage, entry should be gone + var pinningStorage = getCacheStorage("pin", lci); + pinningStorage.asyncEvictStorage(null); + + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "", "", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js new file mode 100644 index 0000000000..bb5ed79b85 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30c-pinning-deferred-doom.js @@ -0,0 +1,188 @@ +/* + +This is a complex test checking the internal "deferred doom" functionality in both CacheEntry and CacheFileHandle. + +- We create a batch of 10 non-pinned and 10 pinned entries, write something to them. +- Then we purge them from memory, so they have to reload from disk. +- After that the IO thread is suspended not to process events on the READ (3) level. This forces opening operation and eviction + sync operations happen before we know actual pinning status of already cached entries. +- We async-open the same batch of the 10+10 entries again, all should open as existing with the expected, previously stored + content +- After all these entries are made to open, we clear the cache. This does some synchronous operations on the entries + being open and also on the handles being in an already open state (but before the entry metadata has started to be read.) + Expected is to leave the pinned entries only. +- Now, we resume the IO thread, so it start reading. One could say this is a hack, but this can very well happen in reality + on slow disk or when a large number of entries is about to be open at once. Suspending the IO thread is just doing this + simulation is a fully deterministic way and actually very easily and elegantly. +- After the resume we want to open all those 10+10 entries once again (no purgin involved this time.). It is expected + to open all the pinning entries intact and loose all the non-pinned entries (get them as new and empty again.) + +*/ + +"use strict"; + +const kENTRYCOUNT = 10; + +function log_(msg) { + if (true) { + dump(">>>>>>>>>>>>> " + msg + "\n"); + } +} + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting); + Assert.ok(testingInterface); + + var mc = new MultipleCallbacks( + 1, + function() { + // (2) + + mc = new MultipleCallbacks(1, finish_cache2_test); + // Release all references to cache entries so that they can be purged + // Calling gc() four times is needed to force it to actually release + // entries that are obviously unreferenced. Yeah, I know, this is wacky... + gc(); + gc(); + executeSoon(() => { + gc(); + gc(); + log_("purging"); + + // Invokes cacheservice:purge-memory-pools when done. + get_cache_service().purgeFromMemory( + Ci.nsICacheStorageService.PURGE_EVERYTHING + ); // goes to (3) + }); + }, + true + ); + + // (1), here we start + + var i; + for (i = 0; i < kENTRYCOUNT; ++i) { + log_("first set of opens"); + + // Callbacks 1-20 + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function(entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function(entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Goes to (2) + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver( + { + observe(subject, topic, data) { + // (3) + + log_("after purge, second set of opens"); + // Prevent the I/O thread from reading the data. We first want to schedule clear of the cache. + // This deterministically emulates a slow hard drive. + testingInterface.suspendCacheIOThread(3); + + // All entries should load + // Callbacks 21-40 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) { + mc.fired(); + }) + ); + + // Unfortunately we cannot ensure that entries existing in the cache will be delivered to the consumer + // when soon after are evicted by some cache API call. It's better to not ensure getting an entry + // than allowing to get an entry that was just evicted from the cache. Entries may be delievered + // as new, but are already doomed. Output stream cannot be openned, or the file handle is already + // writing to a doomed file. + // + // The API now just ensures that entries removed by any of the cache eviction APIs are never more + // available to consumers. + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(MAYBE_NEW | DOOMED, "m" + i, "d" + i, function( + entry + ) { + mc.fired(); + }) + ); + } + + log_("clearing"); + // Now clear everything except pinned, all entries are in state of reading + get_cache_service().clear(); + log_("cleared"); + + // Resume reading the cache data, only now the pinning status on entries will be discovered, + // the deferred dooming code will trigger. + testingInterface.resumeCacheIOThread(); + + log_("third set of opens"); + // Now open again. Pinned entries should be there, disk entries should be the renewed entries. + // Callbacks 41-60 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "m2" + i, "d2" + i, function(entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Finishes this test + }, + }, + "cacheservice:purge-memory-pools" + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js new file mode 100644 index 0000000000..74272072d2 --- /dev/null +++ b/netwerk/test/unit/test_cache2-30d-pinning-WasEvicted-API.js @@ -0,0 +1,151 @@ +/* + +This test exercises the CacheFileContextEvictor::WasEvicted API and code using it. + +- We store 10+10 (pinned and non-pinned) entries to the cache, wait for them being written. +- Then we purge the memory pools. +- Now the IO thread is suspended on the EVICT (7) level to prevent actual deletion of the files. +- Index is disabled. +- We do clear() of the cache, this creates the "ce_*" file and posts to the EVICT level + the eviction loop mechanics. +- We open again those 10+10 entries previously stored. +- IO is resumed +- We expect to get all the pinned and + loose all the non-pinned (common) entries. + +*/ + +"use strict"; + +const kENTRYCOUNT = 10; + +function log_(msg) { + if (true) { + dump(">>>>>>>>>>>>> " + msg + "\n"); + } +} + +function run_test() { + do_get_profile(); + + var lci = Services.loadContextInfo.default; + var testingInterface = get_cache_service().QueryInterface(Ci.nsICacheTesting); + Assert.ok(testingInterface); + + var mc = new MultipleCallbacks( + 1, + function() { + // (2) + + mc = new MultipleCallbacks(1, finish_cache2_test); + // Release all references to cache entries so that they can be purged + // Calling gc() four times is needed to force it to actually release + // entries that are obviously unreferenced. Yeah, I know, this is wacky... + gc(); + gc(); + executeSoon(() => { + gc(); + gc(); + log_("purging"); + + // Invokes cacheservice:purge-memory-pools when done. + get_cache_service().purgeFromMemory( + Ci.nsICacheStorageService.PURGE_EVERYTHING + ); // goes to (3) + }); + }, + true + ); + + // (1), here we start + + log_("first set of opens"); + var i; + for (i = 0; i < kENTRYCOUNT; ++i) { + // Callbacks 1-20 + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "pin", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "p" + i, function(entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + lci, + new OpenCallback(NEW | WAITFORWRITE, "m" + i, "d" + i, function(entry) { + mc.fired(); + }) + ); + } + + mc.fired(); // Goes to (2) + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver( + { + observe(subject, topic, data) { + // (3) + + log_("after purge"); + // Prevent the I/O thread from evicting physically the data. We first want to re-open the entries. + // This deterministically emulates a slow hard drive. + testingInterface.suspendCacheIOThread(7); + + log_("clearing"); + // Now clear everything except pinned. Stores the "ce_*" file and schedules background eviction. + get_cache_service().clear(); + log_("cleared"); + + log_("second set of opens"); + // Now open again. Pinned entries should be there, disk entries should be the renewed entries. + // Callbacks 21-40 + for (i = 0; i < kENTRYCOUNT; ++i) { + mc.add(); + asyncOpenCacheEntry( + "http://pinned" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NORMAL, "m" + i, "p" + i, function(entry) { + mc.fired(); + }) + ); + + mc.add(); + asyncOpenCacheEntry( + "http://common" + i + "/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lci, + new OpenCallback(NEW, "m2" + i, "d2" + i, function(entry) { + mc.fired(); + }) + ); + } + + // Resume IO, this will just pop-off the CacheFileContextEvictor::EvictEntries() because of + // an early check on CacheIOThread::YieldAndRerun() in that method. + // CacheFileIOManager::OpenFileInternal should now run and CacheFileContextEvictor::WasEvicted + // should be checked on. + log_("resuming"); + testingInterface.resumeCacheIOThread(); + log_("resumed"); + + mc.fired(); // Finishes this test + }, + }, + "cacheservice:purge-memory-pools" + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-31-visit-all.js b/netwerk/test/unit/test_cache2-31-visit-all.js new file mode 100644 index 0000000000..b9f1b7eec7 --- /dev/null +++ b/netwerk/test/unit/test_cache2-31-visit-all.js @@ -0,0 +1,88 @@ +"use strict"; + +function run_test() { + getCacheStorage("disk"); + var lcis = [ + Services.loadContextInfo.default, + Services.loadContextInfo.custom(false, { userContextId: 1 }), + Services.loadContextInfo.custom(false, { userContextId: 2 }), + Services.loadContextInfo.custom(false, { userContextId: 3 }), + ]; + + do_get_profile(); + + var mc = new MultipleCallbacks( + 8, + function() { + executeSoon(function() { + var expectedConsumption = 8192; + var entries = [ + { uri: "http://a/", lci: lcis[0] }, // default + { uri: "http://b/", lci: lcis[0] }, // default + { uri: "http://a/", lci: lcis[1] }, // user Context 1 + { uri: "http://b/", lci: lcis[1] }, // user Context 1 + { uri: "http://a/", lci: lcis[2] }, // user Context 2 + { uri: "http://b/", lci: lcis[2] }, // user Context 2 + { uri: "http://a/", lci: lcis[3] }, // user Context 3 + { uri: "http://b/", lci: lcis[3] }, + ]; // user Context 3 + + get_cache_service().asyncVisitAllStorages( + // Test should store 8 entries across 4 originAttributes + new VisitCallback(8, expectedConsumption, entries, function() { + get_cache_service().asyncVisitAllStorages( + // Still 8 entries expected, now don't walk them + new VisitCallback(8, expectedConsumption, null, function() { + finish_cache2_test(); + }), + false + ); + }), + true + ); + }); + }, + true + ); + + // Add two cache entries for each originAttributes. + for (var i = 0; i < lcis.length; i++) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NEW, "a1m", "a1d", function(entry) { + asyncOpenCacheEntry( + "http://a/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NORMAL, "a1m", "a1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NEW, "b1m", "b1d", function(entry) { + asyncOpenCacheEntry( + "http://b/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + lcis[i], + new OpenCallback(NORMAL, "b1m", "b1d", function(entry) { + mc.fired(); + }) + ); + }) + ); + } + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cache2-32-clear-origin.js b/netwerk/test/unit/test_cache2-32-clear-origin.js new file mode 100644 index 0000000000..52987a576f --- /dev/null +++ b/netwerk/test/unit/test_cache2-32-clear-origin.js @@ -0,0 +1,68 @@ +"use strict"; + +const URL = "http://example.net"; +const URL2 = "http://foo.bar"; + +function run_test() { + do_get_profile(); + + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "e1m", "e1d", function(entry) { + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "e1m", "e1d", function(entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "f1m", "f1d", function(entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "f1m", "f1d", function(entry) { + var url = Services.io.newURI(URL); + var principal = Services.scriptSecurityManager.createContentPrincipal( + url, + {} + ); + + get_cache_service().clearOrigin(principal); + + asyncOpenCacheEntry( + URL + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NEW, "e1m", "e1d", function(entry) { + asyncOpenCacheEntry( + URL2 + "/a", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + new OpenCallback(NORMAL, "f1m", "f1d", function(entry) { + finish_cache2_test(); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + }) + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cacheForOfflineUse_no-store.js b/netwerk/test/unit/test_cacheForOfflineUse_no-store.js new file mode 100644 index 0000000000..9d8c14d657 --- /dev/null +++ b/netwerk/test/unit/test_cacheForOfflineUse_no-store.js @@ -0,0 +1,104 @@ +"use strict"; +// https://bugzilla.mozilla.org/show_bug.cgi?id=760955 + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +const testFileName = "test_nsHttpChannel_CacheForOfflineUse-no-store"; +const cacheClientID = testFileName + "|fake-group-id"; +const basePath = "/" + testFileName + "/"; + +XPCOMUtils.defineLazyGetter(this, "baseURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + basePath; +}); + +const normalEntry = "normal"; +const noStoreEntry = "no-store"; + +var cacheUpdateObserver = null; +var appCache = null; + +function make_channel_for_offline_use(url, callback, ctx) { + var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + + var cacheService = Cc[ + "@mozilla.org/network/application-cache-service;1" + ].getService(Ci.nsIApplicationCacheService); + appCache = cacheService.getApplicationCache(cacheClientID); + + var appCacheChan = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + appCacheChan.applicationCacheForWrite = appCache; + return chan; +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +const responseBody = "response body"; + +// A HTTP channel for updating the offline cache should normally succeed. +function normalHandler(metadata, response) { + info("normalHandler"); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} +function checkNormal(request, buffer) { + Assert.equal(buffer, responseBody); + asyncCheckCacheEntryPresence( + baseURI + normalEntry, + "appcache", + true, + run_next_test, + appCache + ); +} +add_test(function test_normal() { + var chan = make_channel_for_offline_use(baseURI + normalEntry); + chan.asyncOpen(new ChannelListener(checkNormal, chan)); +}); + +// An HTTP channel for updating the offline cache should fail when it gets a +// response with Cache-Control: no-store. +function noStoreHandler(metadata, response) { + info("noStoreHandler"); + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Cache-Control", "no-store"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} +function checkNoStore(request, buffer) { + Assert.equal(buffer, ""); + asyncCheckCacheEntryPresence( + baseURI + noStoreEntry, + "appcache", + false, + run_next_test, + appCache + ); +} +add_test(function test_noStore() { + var chan = make_channel_for_offline_use(baseURI + noStoreEntry); + // The no-store should cause the channel to fail to load. + chan.asyncOpen(new ChannelListener(checkNoStore, chan, CL_EXPECT_FAILURE)); +}); + +function run_test() { + do_get_profile(); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + + httpServer = new HttpServer(); + httpServer.registerPathHandler(basePath + normalEntry, normalHandler); + httpServer.registerPathHandler(basePath + noStoreEntry, noStoreHandler); + httpServer.start(-1); + run_next_test(); +} + +function finish_test(request, buffer) { + httpServer.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_cache_204_response.js b/netwerk/test/unit/test_cache_204_response.js new file mode 100644 index 0000000000..aeb4f4f1d2 --- /dev/null +++ b/netwerk/test/unit/test_cache_204_response.js @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +Test if 204 response is cached. +1. Make first http request and return a 204 response. +2. Check if the first response is not cached. +3. Make second http request and check if the response is cached. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-control", "max-age=9999", false); + response.setStatusLine(metadata.httpVersion, 204, "No Content"); +} + +function make_channel(url) { + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return channel; +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(); + }) + ); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +add_task(async function() { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + await get_response(make_channel(URI, "GET"), false); + await get_response(make_channel(URI, "GET"), true); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_cache_jar.js b/netwerk/test/unit/test_cache_jar.js new file mode 100644 index 0000000000..554759be21 --- /dev/null +++ b/netwerk/test/unit/test_cache_jar.js @@ -0,0 +1,104 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort + "/cached"; +}); + +var httpserv = null; +var handlers_called = 0; + +function cached_handler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "max-age=10000", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + var body = "0123456789"; + response.bodyOutputStream.write(body, body.length); + handlers_called++; +} + +function makeChan(url, inIsolatedMozBrowser, userContextId) { + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadInfo.originAttributes = { inIsolatedMozBrowser, userContextId }; + return chan; +} + +// [inIsolatedMozBrowser, userContextId, expected_handlers_called] +var firstTests = [ + [false, 0, 1], + [true, 0, 1], + [false, 1, 1], + [true, 1, 1], +]; +var secondTests = [ + [false, 0, 0], + [true, 0, 0], + [false, 1, 1], + [true, 1, 0], +]; + +async function run_all_tests() { + for (let test of firstTests) { + handlers_called = 0; + await test_channel(...test); + } + + // We can't easily cause webapp data to be cleared from the child process, so skip + // the rest of these tests. + let procType = Cc["@mozilla.org/xre/runtime;1"].getService(Ci.nsIXULRuntime) + .processType; + if (procType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) { + return; + } + + Services.clearData.deleteDataFromOriginAttributesPattern({ + userContextId: 1, + }); + + for (let test of secondTests) { + handlers_called = 0; + await test_channel(...test); + } +} + +function run_test() { + do_get_profile(); + + do_test_pending(); + + Services.prefs.setBoolPref("network.http.rcwn.enabled", false); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/cached", cached_handler); + httpserv.start(-1); + run_all_tests().then(() => { + do_test_finished(); + }); +} + +function test_channel(inIsolatedMozBrowser, userContextId, expected) { + return new Promise(resolve => { + var chan = makeChan(URL, inIsolatedMozBrowser, userContextId); + chan.asyncOpen( + new ChannelListener(doneFirstLoad.bind(null, resolve), expected) + ); + }); +} + +function doneFirstLoad(resolve, req, buffer, expected) { + // Load it again, make sure it hits the cache + var oa = req.loadInfo.originAttributes; + var chan = makeChan(URL, oa.isInIsolatedMozBrowserElement, oa.userContextId); + chan.asyncOpen( + new ChannelListener(doneSecondLoad.bind(null, resolve), expected) + ); +} + +function doneSecondLoad(resolve, req, buffer, expected) { + Assert.equal(handlers_called, expected); + resolve(); +} diff --git a/netwerk/test/unit/test_cacheflags.js b/netwerk/test/unit/test_cacheflags.js new file mode 100644 index 0000000000..4c0ae215f5 --- /dev/null +++ b/netwerk/test/unit/test_cacheflags.js @@ -0,0 +1,436 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var httpserver = new HttpServer(); +httpserver.start(-1); + +// Need to randomize, because apparently no one clears our cache +var suffix = Math.random(); +var httpBase = "http://localhost:" + httpserver.identity.primaryPort; +var httpsBase = "http://localhost:4445"; +var shortexpPath = "/shortexp" + suffix; +var longexpPath = "/longexp/" + suffix; +var longexp2Path = "/longexp/2/" + suffix; +var nocachePath = "/nocache" + suffix; +var nostorePath = "/nostore" + suffix; +var test410Path = "/test410" + suffix; +var test404Path = "/test404" + suffix; + +var PrivateBrowsingLoadContext = Cu.createPrivateLoadContext(); + +function make_channel(url, flags, usePrivateBrowsing) { + var securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + + var uri = Services.io.newURI(url); + var principal = Services.scriptSecurityManager.createContentPrincipal(uri, { + privateBrowsingId: usePrivateBrowsing ? 1 : 0, + }); + + var req = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + req.loadFlags = flags; + if (usePrivateBrowsing) { + req.notificationCallbacks = PrivateBrowsingLoadContext; + } + return req; +} + +function Test( + path, + flags, + expectSuccess, + readFromCache, + hitServer, + usePrivateBrowsing /* defaults to false */ +) { + this.path = path; + this.flags = flags; + this.expectSuccess = expectSuccess; + this.readFromCache = readFromCache; + this.hitServer = hitServer; + this.usePrivateBrowsing = usePrivateBrowsing; +} + +Test.prototype = { + flags: 0, + expectSuccess: true, + readFromCache: false, + hitServer: true, + usePrivateBrowsing: false, + _buffer: "", + _isFromCache: false, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + var cachingChannel = request.QueryInterface(Ci.nsICacheInfoChannel); + this._isFromCache = request.isPending() && cachingChannel.isFromCache(); + }, + + onDataAvailable(request, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + Assert.equal(Components.isSuccessCode(status), this.expectSuccess); + Assert.equal(this._isFromCache, this.readFromCache); + Assert.equal(gHitServer, this.hitServer); + + do_timeout(0, run_next_test); + }, + + run() { + dump( + "Running:" + + "\n " + + this.path + + "\n " + + this.flags + + "\n " + + this.expectSuccess + + "\n " + + this.readFromCache + + "\n " + + this.hitServer + + "\n" + ); + gHitServer = false; + var channel = make_channel(this.path, this.flags, this.usePrivateBrowsing); + channel.asyncOpen(this); + }, +}; + +var gHitServer = false; + +var gTests = [ + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + false, // read from cache + true, // hit server + true + ), // USE PRIVATE BROWSING, so not cached for later requests + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + 0, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsIRequest.LOAD_BYPASS_CACHE, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + shortexpPath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + longexpPath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.LOAD_BYPASS_CACHE, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.VALIDATE_ALWAYS, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_ALWAYS, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + longexpPath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + longexp2Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + longexp2Path, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + nocachePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nocachePath, + 0, + true, // expect success + true, // read from cache + true + ), // hit server + new Test( + httpBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + + // CACHE2: mayhemer - entry is doomed... I think the logic is wrong, we should not doom them + // as they are not valid, but take them as they need to reval + /* + new Test(httpBase + nocachePath, Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false), // hit server + */ + + // LOAD_ONLY_FROM_CACHE would normally fail (because no-cache forces + // a validation), but VALIDATE_NEVER should override that. + new Test( + httpBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + true, // expect success + true, // read from cache + false + ), // hit server + + // ... however, no-cache over ssl should act like no-store and force + // a validation (and therefore failure) even if VALIDATE_NEVER is + // set. + /* XXX bug 466524: We can't currently start an ssl server in xpcshell tests, + so this test is currently disabled. + new Test(httpsBase + nocachePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | + Ci.nsIRequest.VALIDATE_NEVER, + false, // expect success + false, // read from cache + false) // hit server + */ + + new Test( + httpBase + nostorePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nostorePath, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + nostorePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE, + false, // expect success + false, // read from cache + false + ), // hit server + new Test( + httpBase + nostorePath, + Ci.nsIRequest.LOAD_FROM_CACHE, + true, // expect success + true, // read from cache + false + ), // hit server + // no-store should force the validation (and therefore failure, with + // LOAD_ONLY_FROM_CACHE) even if VALIDATE_NEVER is set. + new Test( + httpBase + nostorePath, + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | Ci.nsIRequest.VALIDATE_NEVER, + false, // expect success + false, // read from cache + false + ), // hit server + + new Test( + httpBase + test410Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + test410Path, + 0, + true, // expect success + true, // read from cache + false + ), // hit server + + new Test( + httpBase + test404Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server + new Test( + httpBase + test404Path, + 0, + true, // expect success + false, // read from cache + true + ), // hit server +]; + +function run_next_test() { + if (gTests.length == 0) { + httpserver.stop(do_test_finished); + return; + } + + var test = gTests.shift(); + test.run(); +} + +function handler(httpStatus, metadata, response) { + gHitServer = true; + try { + var etag = metadata.getHeader("If-None-Match"); + } catch (ex) { + var etag = ""; + } + if (etag == "testtag") { + // Allow using the cached data + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + } else { + response.setStatusLine(metadata.httpVersion, httpStatus, "Useless Phrase"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "testtag", false); + const body = "data"; + response.bodyOutputStream.write(body, body.length); + } +} + +function nocache_handler(metadata, response) { + response.setHeader("Cache-Control", "no-cache", false); + handler(200, metadata, response); +} + +function nostore_handler(metadata, response) { + response.setHeader("Cache-Control", "no-store", false); + handler(200, metadata, response); +} + +function test410_handler(metadata, response) { + handler(410, metadata, response); +} + +function test404_handler(metadata, response) { + handler(404, metadata, response); +} + +function shortexp_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age=0", false); + handler(200, metadata, response); +} + +function longexp_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age=10000", false); + handler(200, metadata, response); +} + +// test spaces around max-age value token +function longexp2_handler(metadata, response) { + response.setHeader("Cache-Control", "max-age = 10000", false); + handler(200, metadata, response); +} + +function run_test() { + httpserver.registerPathHandler(shortexpPath, shortexp_handler); + httpserver.registerPathHandler(longexpPath, longexp_handler); + httpserver.registerPathHandler(longexp2Path, longexp2_handler); + httpserver.registerPathHandler(nocachePath, nocache_handler); + httpserver.registerPathHandler(nostorePath, nostore_handler); + httpserver.registerPathHandler(test410Path, test410_handler); + httpserver.registerPathHandler(test404Path, test404_handler); + + run_next_test(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_captive_portal_service.js b/netwerk/test/unit/test_captive_portal_service.js new file mode 100644 index 0000000000..55ca0874b9 --- /dev/null +++ b/netwerk/test/unit/test_captive_portal_service.js @@ -0,0 +1,207 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver = null; +XPCOMUtils.defineLazyGetter(this, "cpURI", function() { + return "http://localhost:" + httpserver.identity.primaryPort + "/captive.txt"; +}); + +const SUCCESS_STRING = "success\n"; +let cpResponse = SUCCESS_STRING; +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(cpResponse, cpResponse.length); +} + +const PREF_CAPTIVE_ENABLED = "network.captive-portal-service.enabled"; +const PREF_CAPTIVE_TESTMODE = "network.captive-portal-service.testMode"; +const PREF_CAPTIVE_ENDPOINT = "captivedetect.canonicalURL"; +const PREF_CAPTIVE_MINTIME = "network.captive-portal-service.minInterval"; +const PREF_CAPTIVE_MAXTIME = "network.captive-portal-service.maxInterval"; +const PREF_DNS_NATIVE_IS_LOCALHOST = "network.dns.native-is-localhost"; + +const cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService +); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref(PREF_CAPTIVE_ENABLED); + Services.prefs.clearUserPref(PREF_CAPTIVE_TESTMODE); + Services.prefs.clearUserPref(PREF_CAPTIVE_ENDPOINT); + Services.prefs.clearUserPref(PREF_CAPTIVE_MINTIME); + Services.prefs.clearUserPref(PREF_CAPTIVE_MAXTIME); + Services.prefs.clearUserPref(PREF_DNS_NATIVE_IS_LOCALHOST); + + await new Promise(resolve => { + httpserver.stop(resolve); + }); +}); + +function observerPromise(topic) { + return new Promise(resolve => { + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + resolve(aData); + } + }, + }; + Services.obs.addObserver(observer, topic); + }); +} + +add_task(function setup() { + httpserver = new HttpServer(); + httpserver.registerPathHandler("/captive.txt", contentHandler); + httpserver.start(-1); + + Services.prefs.setCharPref(PREF_CAPTIVE_ENDPOINT, cpURI); + Services.prefs.setIntPref(PREF_CAPTIVE_MINTIME, 50); + Services.prefs.setIntPref(PREF_CAPTIVE_MAXTIME, 100); + Services.prefs.setBoolPref(PREF_CAPTIVE_TESTMODE, true); + Services.prefs.setBoolPref(PREF_DNS_NATIVE_IS_LOCALHOST, true); +}); + +add_task(async function test_simple() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + let notification = observerPromise("network:captive-portal-connectivity"); + // The service is started by nsIOService when the pref becomes true. + // We might want to add a method to do this in the future. + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + let observerPayload = await notification; + equal(observerPayload, "clear"); + equal(cps.state, Ci.nsICaptivePortalService.NOT_CAPTIVE); + + cpResponse = "other"; + notification = observerPromise("captive-portal-login"); + cps.recheckCaptivePortal(); + await notification; + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); + + cpResponse = SUCCESS_STRING; + notification = observerPromise("captive-portal-login-success"); + cps.recheckCaptivePortal(); + await notification; + equal(cps.state, Ci.nsICaptivePortalService.UNLOCKED_PORTAL); +}); + +// This test redirects to another URL which returns the same content. +// It should still be interpreted as a captive portal. +add_task(async function test_redirect_success() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/succ.txt", (metadata, response) => { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(cpResponse, cpResponse.length); + }); + httpserver.registerPathHandler("/captive.txt", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader( + "Location", + `http://localhost:${httpserver.identity.primaryPort}/succ.txt` + ); + }); + + let notification = observerPromise("captive-portal-login").then( + () => "login" + ); + let succNotif = observerPromise("network:captive-portal-connectivity").then( + () => "connectivity" + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + let winner = await Promise.race([notification, succNotif]); + equal(winner, "login", "This should have been a login, not a success"); + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to same text" + ); +}); + +// This redirects to another URI with a different content. +// We check that it triggers a captive portal login +add_task(async function test_redirect_bad() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + httpserver.registerPathHandler("/bad.txt", (metadata, response) => { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("bad", "bad".length); + }); + + httpserver.registerPathHandler("/captive.txt", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader( + "Location", + `http://localhost:${httpserver.identity.primaryPort}/bad.txt` + ); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to bad text" + ); +}); + +// This redirects to the same URI. +// We check that it triggers a captive portal login +add_task(async function test_redirect_loop() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + // This is actually a redirect loop + httpserver.registerPathHandler("/captive.txt", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", cpURI); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal(cps.state, Ci.nsICaptivePortalService.LOCKED_PORTAL); +}); + +// This redirects to a https URI. +// We check that it triggers a captive portal login +add_task(async function test_redirect_https() { + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); + equal(cps.state, Ci.nsICaptivePortalService.UNKNOWN); + + let h2Port = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment) + .get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Any kind of redirection should trigger the captive portal login. + httpserver.registerPathHandler("/captive.txt", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", `https://foo.example.com:${h2Port}/exit`); + }); + + let notification = observerPromise("captive-portal-login"); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, true); + + await notification; + equal( + cps.state, + Ci.nsICaptivePortalService.LOCKED_PORTAL, + "Should be locked after redirect to https" + ); + Services.prefs.setBoolPref(PREF_CAPTIVE_ENABLED, false); +}); diff --git a/netwerk/test/unit/test_channel_close.js b/netwerk/test/unit/test_channel_close.js new file mode 100644 index 0000000000..e05f0e9bb2 --- /dev/null +++ b/netwerk/test/unit/test_channel_close.js @@ -0,0 +1,68 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpProtocolHandler = Cc[ + "@mozilla.org/network/protocol;1?name=http" +].getService(Ci.nsIHttpProtocolHandler); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; + +var live_channels = []; + +function run_test() { + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + httpProtocolHandler.EnsureHSTSDataReady().then(function() { + var local_channel; + + // Opened channel that has no remaining references on shutdown + local_channel = setupChannel(testpath); + local_channel.asyncOpen(new ChannelListener(checkRequest, local_channel)); + + // Opened channel that has no remaining references after being opened + setupChannel(testpath).asyncOpen(new ChannelListener(function() {}, null)); + + // Unopened channel that has remaining references on shutdown + live_channels.push(setupChannel(testpath)); + + // Opened channel that has remaining references on shutdown + live_channels.push(setupChannel(testpath)); + live_channels[1].asyncOpen( + new ChannelListener(checkRequestFinish, live_channels[1]) + ); + }); + + do_test_pending(); +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} + +function checkRequest(request, data, context) { + Assert.equal(data, httpbody); +} + +function checkRequestFinish(request, data, context) { + checkRequest(request, data, context); + httpserver.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_channel_priority.js b/netwerk/test/unit/test_channel_priority.js new file mode 100644 index 0000000000..2a757de90a --- /dev/null +++ b/netwerk/test/unit/test_channel_priority.js @@ -0,0 +1,97 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* globals NetUtil*/ +/* globals HttpServer */ +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver; +let port; + +function startHttpServer() { + httpserver = new HttpServer(); + + httpserver.registerPathHandler("/resource", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Cache-Control", "no-cache", false); + response.bodyOutputStream.write("data", 4); + }); + + httpserver.registerPathHandler("/redirect", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 302, "Redirect"); + response.setHeader("Location", "/resource", false); + response.setHeader("Cache-Control", "no-cache", false); + }); + + httpserver.start(-1); + port = httpserver.identity.primaryPort; +} + +function stopHttpServer() { + httpserver.stop(() => {}); +} + +function makeRequest(uri) { + let requestChannel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + requestChannel.QueryInterface(Ci.nsISupportsPriority); + requestChannel.priority = Ci.nsISupportsPriority.PRIORITY_HIGHEST; + requestChannel.asyncOpen(new ChannelListener(checkResponse, requestChannel)); +} + +function checkResponse(request, buffer, requestChannel) { + requestChannel.QueryInterface(Ci.nsISupportsPriority); + Assert.equal( + requestChannel.priority, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + + // the response channel can be different (if it was redirected) + let responseChannel = request.QueryInterface(Ci.nsISupportsPriority); + Assert.equal( + responseChannel.priority, + Ci.nsISupportsPriority.PRIORITY_HIGHEST + ); + + run_next_test(); +} + +add_test(function test_regular_request() { + makeRequest(`http://localhost:${port}/resource`); +}); + +add_test(function test_redirect() { + makeRequest(`http://localhost:${port}/redirect`); +}); + +function run_test() { + // jshint ignore:line + if (!runningInParent) { + // add a task to report test finished to parent process at the end of test queue, + // since do_register_cleanup is not available in child xpcshell test script. + add_test(function() { + do_send_remote_message("finished"); + run_next_test(); + }); + + // waiting for parent process to assign server port via configPort() + return; + } + + startHttpServer(); + registerCleanupFunction(stopHttpServer); + run_next_test(); +} + +// This is used by unit_ipc/test_channel_priority_wrap.js for e10s XPCShell test +function configPort(serverPort) { + // jshint ignore:line + port = serverPort; + run_next_test(); +} diff --git a/netwerk/test/unit/test_chunked_responses.js b/netwerk/test/unit/test_chunked_responses.js new file mode 100644 index 0000000000..ea0149e5c5 --- /dev/null +++ b/netwerk/test/unit/test_chunked_responses.js @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test Chunked-Encoded response parsing. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = []; +var testPathBase = "/chunked_hdrs"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + var testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + var flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(eval("completeTest" + num), channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of overflowed chunked size. The parser uses long so +// the test case uses >64bit to fail on all platforms. +test_flags[1] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler1(metadata, response) { + var body = "12345678123456789\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: FAIL because of non-hex in chunked length + +test_flags[2] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler2(metadata, response) { + var body = "junkintheway 123\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest2(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: OK in spite of non-hex digits after size in the length field + +test_flags[3] = CL_ALLOW_UNKNOWN_CL; + +function handler3(metadata, response) { + var body = "c junkafter\r\ndata reached\r\n0\r\n\r\n"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest3(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: Verify a fully compliant chunked response. + +test_flags[4] = CL_ALLOW_UNKNOWN_CL; + +function handler4(metadata, response) { + var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n\r\n"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest4(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(5); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: A chunk size larger than 32 bit but smaller than 64bit also fails +// This is probabaly subject to get improved at some point. + +test_flags[5] = CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler5(metadata, response) { + var body = "123456781\r\ndata never reached"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Transfer-Encoding: chunked\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest5(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_UNEXPECTED); + endTests(); + // run_test_number(6); +} diff --git a/netwerk/test/unit/test_compareURIs.js b/netwerk/test/unit/test_compareURIs.js new file mode 100644 index 0000000000..c2899b33bc --- /dev/null +++ b/netwerk/test/unit/test_compareURIs.js @@ -0,0 +1,60 @@ +"use strict"; + +function do_info(text, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + dump( + "TEST-INFO | " + + stack.filename + + " | [" + + stack.name + + " : " + + stack.lineNumber + + "] " + + text + + "\n" + ); +} +function run_test() { + var tests = [ + ["http://mozilla.org/", "http://mozilla.org/somewhere/there", true], + ["http://mozilla.org/", "http://www.mozilla.org/", false], + ["http://mozilla.org/", "http://mozilla.org:80", true], + ["http://mozilla.org/", "http://mozilla.org:90", false], + ["http://mozilla.org", "https://mozilla.org", false], + ["http://mozilla.org", "https://mozilla.org:80", false], + ["http://mozilla.org:443", "https://mozilla.org", false], + ["https://mozilla.org:443", "https://mozilla.org", true], + ["https://mozilla.org:443", "https://mozilla.org/somewhere/", true], + ["about:", "about:", false], + ["data:text/plain,text", "data:text/plain,text", false], + ["about:blank", "about:blank", false], + ["about:", "http://mozilla.org/", false], + ["about:", "about:config", false], + ["about:text/plain,text", "data:text/plain,text", false], + ["jar:http://mozilla.org/!/", "http://mozilla.org/", true], + ["view-source:http://mozilla.org/", "http://mozilla.org/", true], + ]; + + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + + tests.forEach(function(aTest) { + do_info("Comparing " + aTest[0] + " to " + aTest[1]); + + var uri1 = NetUtil.newURI(aTest[0]); + var uri2 = NetUtil.newURI(aTest[1]); + + var equal; + try { + secman.checkSameOriginURI(uri1, uri2, false, false); + equal = true; + } catch (e) { + equal = false; + } + Assert.equal(equal, aTest[2]); + }); +} diff --git a/netwerk/test/unit/test_compressappend.js b/netwerk/test/unit/test_compressappend.js new file mode 100644 index 0000000000..1374eb01ab --- /dev/null +++ b/netwerk/test/unit/test_compressappend.js @@ -0,0 +1,99 @@ +// +// Test that data can be appended to a cache entry even when the data is +// compressed by the cache compression feature - bug 648429. +// + +"use strict"; + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function TestAppend(compress, callback) { + this._compress = compress; + this._callback = callback; + this.run(); +} + +TestAppend.prototype = { + _compress: false, + _callback: null, + + run() { + evict_cache_entries(); + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + this.writeData.bind(this) + ); + }, + + writeData(status, entry) { + Assert.equal(status, Cr.NS_OK); + if (this._compress) { + entry.setMetaDataElement("uncompressed-len", "0"); + } + var os = entry.openOutputStream(0, 5); + write_and_check(os, "12345", 5); + os.close(); + entry.close(); + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + this.appendData.bind(this) + ); + }, + + appendData(status, entry) { + Assert.equal(status, Cr.NS_OK); + var os = entry.openOutputStream(entry.storageDataSize, 5); + write_and_check(os, "abcde", 5); + os.close(); + entry.close(); + + asyncOpenCacheEntry( + "http://data/", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + this.checkData.bind(this) + ); + }, + + checkData(status, entry) { + Assert.equal(status, Cr.NS_OK); + var self = this; + pumpReadStream(entry.openInputStream(0), function(str) { + Assert.equal(str.length, 10); + Assert.equal(str, "12345abcde"); + entry.close(); + + executeSoon(self._callback); + }); + }, +}; + +function run_test() { + do_get_profile(); + new TestAppend(false, run_test2); + do_test_pending(); +} + +function run_test2() { + new TestAppend(true, do_test_finished); +} diff --git a/netwerk/test/unit/test_content_encoding_gzip.js b/netwerk/test/unit/test_content_encoding_gzip.js new file mode 100644 index 0000000000..a3fb41e76c --- /dev/null +++ b/netwerk/test/unit/test_content_encoding_gzip.js @@ -0,0 +1,210 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +var index = 0; +var tests = [ + { + url: "/test/cegzip1", + flags: CL_EXPECT_GZIP, + ce: "gzip", + body: [ + 0x1f, + 0x8b, + 0x08, + 0x08, + 0x5a, + 0xa0, + 0x31, + 0x4f, + 0x00, + 0x03, + 0x74, + 0x78, + 0x74, + 0x00, + 0x2b, + 0xc9, + 0xc8, + 0x2c, + 0x56, + 0x00, + 0xa2, + 0x92, + 0xd4, + 0xe2, + 0x12, + 0x43, + 0x2e, + 0x00, + 0xb9, + 0x23, + 0xd7, + 0x3b, + 0x0e, + 0x00, + 0x00, + 0x00, + ], + datalen: 14, // the data length of the uncompressed document + }, + + { + url: "/test/cegzip2", + flags: CL_EXPECT_GZIP, + ce: "gzip, gzip", + body: [ + 0x1f, + 0x8b, + 0x08, + 0x00, + 0x72, + 0xa1, + 0x31, + 0x4f, + 0x00, + 0x03, + 0x93, + 0xef, + 0xe6, + 0xe0, + 0x88, + 0x5a, + 0x60, + 0xe8, + 0xcf, + 0xc0, + 0x5c, + 0x52, + 0x51, + 0xc2, + 0xa0, + 0x7d, + 0xf2, + 0x84, + 0x4e, + 0x18, + 0xc3, + 0xa2, + 0x49, + 0x57, + 0x1e, + 0x09, + 0x39, + 0xeb, + 0x31, + 0xec, + 0x54, + 0xbe, + 0x6e, + 0xcd, + 0xc7, + 0xc0, + 0xc0, + 0x00, + 0x00, + 0x6e, + 0x90, + 0x7a, + 0x85, + 0x24, + 0x00, + 0x00, + 0x00, + ], + datalen: 14, // the data length of the uncompressed document + }, + + { + url: "/test/cebrotli1", + flags: CL_EXPECT_GZIP, + ce: "br", + body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03], + + datalen: 5, // the data length of the uncompressed document + }, + + // this is not a brotli document + { + url: "/test/cebrotli2", + flags: CL_EXPECT_GZIP | CL_EXPECT_FAILURE, + ce: "br", + body: [0x0b, 0x0a, 0x09], + datalen: 3, + }, + + // this is brotli but should come through as identity due to prefs + { + url: "/test/cebrotli3", + flags: 0, + ce: "br", + body: [0x0b, 0x02, 0x80, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x03], + + datalen: 9, + }, +]; + +function setupChannel(url) { + return NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + url, + loadUsingSystemPrincipal: true, + }); +} + +function startIter() { + if (tests[index].url === "/test/cebrotli3") { + // this test wants to make sure we don't do brotli when not in a-e + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate"); + } + var channel = setupChannel(tests[index].url); + channel.asyncOpen( + new ChannelListener(completeIter, channel, tests[index].flags) + ); +} + +function completeIter(request, data, ctx) { + if (!(tests[index].flags & CL_EXPECT_FAILURE)) { + Assert.equal(data.length, tests[index].datalen); + } + if (++index < tests.length) { + startIter(); + } else { + httpserver.stop(do_test_finished); + prefs.setCharPref("network.http.accept-encoding", cePref); + } +} + +var prefs; +var cePref; +function run_test() { + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + cePref = prefs.getCharPref("network.http.accept-encoding"); + prefs.setCharPref("network.http.accept-encoding", "gzip, deflate, br"); + + httpserver.registerPathHandler("/test/cegzip1", handler); + httpserver.registerPathHandler("/test/cegzip2", handler); + httpserver.registerPathHandler("/test/cebrotli1", handler); + httpserver.registerPathHandler("/test/cebrotli2", handler); + httpserver.registerPathHandler("/test/cebrotli3", handler); + httpserver.start(-1); + + startIter(); + do_test_pending(); +} + +function handler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Encoding", tests[index].ce, false); + response.setHeader("Content-Length", "" + tests[index].body.length, false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(tests[index].body); + response.finish(); +} diff --git a/netwerk/test/unit/test_content_length_underrun.js b/netwerk/test/unit/test_content_length_underrun.js new file mode 100644 index 0000000000..b651d36065 --- /dev/null +++ b/netwerk/test/unit/test_content_length_underrun.js @@ -0,0 +1,280 @@ +/* + * Test Content-Length underrun behavior + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = []; +var testPathBase = "/cl_hdrs"; + +var prefs; +var enforcePrefStrict; +var enforcePrefSoft; +var enforcePrefStrictChunked; + +Services.prefs.setBoolPref("security.allow_eval_with_system_principal", true); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("security.allow_eval_with_system_principal"); +}); + +function run_test() { + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + enforcePrefStrict = prefs.getBoolPref("network.http.enforce-framing.http1"); + enforcePrefSoft = prefs.getBoolPref("network.http.enforce-framing.soft"); + enforcePrefStrictChunked = prefs.getBoolPref( + "network.http.enforce-framing.strict_chunked_encoding" + ); + + prefs.setBoolPref("network.http.enforce-framing.http1", true); + + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(eval("completeTest" + num), channel, flags) + ); +} + +function run_gzip_test(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, eval("handler" + num)); + + var channel = setupChannel(testPath); + + function StreamListener() {} + + StreamListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(aRequest) {}, + + onStopRequest(aRequest, aStatusCode) { + // Make sure we catch the error NS_ERROR_NET_PARTIAL_TRANSFER here. + Assert.equal(aStatusCode, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + // do_test_finished(); + endTests(); + }, + + onDataAvailable(request, stream, offset, count) {}, + }; + + let listener = new StreamListener(); + + channel.asyncOpen(listener); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + // restore the prefs to pre-test values + prefs.setBoolPref("network.http.enforce-framing.http1", enforcePrefStrict); + prefs.setBoolPref("network.http.enforce-framing.soft", enforcePrefSoft); + prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + enforcePrefStrictChunked + ); + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of Content-Length underrun with HTTP 1.1 +test_flags[1] = CL_EXPECT_LATE_FAILURE; + +function handler1(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + + run_test_number(11); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 11: PASS because of Content-Length underrun with HTTP 1.1 but non 2xx +test_flags[11] = CL_IGNORE_CL; + +function handler11(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 404 NotOK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest11(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: Succeed because Content-Length underrun is with HTTP 1.0 + +test_flags[2] = CL_IGNORE_CL; + +function handler2(metadata, response) { + var body = "short content"; + + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 12345678\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest2(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + + // test 3 requires the enforce-framing prefs to be false + prefs.setBoolPref("network.http.enforce-framing.http1", false); + prefs.setBoolPref("network.http.enforce-framing.soft", false); + prefs.setBoolPref( + "network.http.enforce-framing.strict_chunked_encoding", + false + ); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: SUCCEED with bad Content-Length because pref allows it +test_flags[3] = CL_IGNORE_CL; + +function handler3(metadata, response) { + var body = "blablabla"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 556677\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest3(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + prefs.setBoolPref("network.http.enforce-framing.soft", true); + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: Succeed because a cut off deflate stream can't be detected +test_flags[4] = CL_IGNORE_CL; + +function handler4(metadata, response) { + // this is the beginning of a deflate compressed response body + + var body = + "\xcd\x57\xcd\x6e\x1b\x37\x10\xbe\x07\xc8\x3b\x0c\x36\x68\x72\xd1" + + "\xbf\x92\x22\xb1\x57\x0a\x64\x4b\x6a\x0c\x28\xb6\x61\xa9\x41\x73" + + "\x2a\xb8\xbb\x94\x44\x98\xfb\x03\x92\x92\xec\x06\x7d\x97\x1e\xeb" + + "\xbe\x86\x5e\xac\xc3\x25\x97\xa2\x64\xb9\x75\x0b\x14\xe8\x69\x87" + + "\x33\x9c\x1f\x7e\x33\x9c\xe1\x86\x9f\x66\x9f\x27\xfd\x97\x2f\x20" + + "\xfc\x34\x1a\x0c\x35\x01\xa1\x62\x8a\xd3\xfe\xf5\xcd\xd5\xe5\xd5" + + "\x6c\x54\x83\x49\xbe\x60\x31\xa3\x1c\x12\x0a\x0b\x2a\x15\xcb\x33" + + "\x4d\xae\x19\x05\x19\xe7\x9c\x30\x41\x1b\x61\xd3\x28\x95\xfa\x29" + + "\x55\x04\x32\x92\xd2\x5e\x90\x50\x19\x0b\x56\x68\x9d\x00\xe2\x3c" + + "\x53\x34\x53\xbd\xc0\x99\x56\xf9\x4a\x51\xe0\x64\xcf\x18\x24\x24" + + "\x93\xb0\xca\x40\xd2\x15\x07\x6e\xbd\x37\x60\x82\x3b\x8f\x86\x22" + + "\x21\xcb\x15\x95\x35\x20\x91\xa4\x59\xac\xa9\x62\x95\x31\xed\x14" + + "\xc9\x98\x2c\x19\x15\x3a\x62\x45\xef\x70\x1b\x50\x05\xa4\x28\xc4" + + "\xf6\x21\x66\xa4\xdc\x83\x32\x09\x85\xc8\xe7\x54\xa2\x4b\x81\x74" + + "\xbe\x12\xc0\x91\xb9\x7d\x50\x24\xe2\x0c\xd9\x29\x06\x2e\xdd\x79"; + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 553677\r\n"); + response.write("Content-Encoding: deflate\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest4(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + + prefs.setBoolPref("network.http.enforce-framing.http1", true); + run_gzip_test(99); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 99: FAIL because a cut off gzip stream CAN be detected + +// Note that test 99 here is run completely different than the other tests in +// this file so if you add more tests here, consider adding them before this. + +function handler99(metadata, response) { + // this is the beginning of a gzip compressed response body + + var body = + "\x1f\x8b\x08\x00\x80\xb9\x25\x53\x00\x03\xd4\xd9\x79\xb8\x8e\xe5" + + "\xba\x00\xf0\x65\x19\x33\x24\x15\x29\xf3\x50\x52\xc6\xac\x85\x10" + + "\x8b\x12\x22\x45\xe6\xb6\x21\x9a\x96\x84\x4c\x69\x32\xec\x84\x92" + + "\xcc\x99\x6a\xd9\x32\xa5\xd0\x40\xd9\xc6\x14\x15\x95\x28\x62\x9b" + + "\x09\xc9\x70\x4a\x25\x53\xec\x8e\x9c\xe5\x1c\x9d\xeb\xfe\x9d\x73" + + "\x9d\x3f\xf6\x1f\xe7\xbd\xae\xcf\xf3\xbd\xbf\xef\x7e\x9f\xeb\x79" + + "\xef\xf7\x99\xde\xe5\xee\x6e\xdd\x3b\x75\xeb\xd1\xb5\x6c\xb3\xd4" + + "\x47\x1f\x48\xf8\x17\x1d\x15\xce\x1d\x55\x92\x93\xcf\x97\xe7\x8e" + + "\x8b\xca\xe4\xca\x55\x92\x2a\x54\x4e\x4e\x4e\x4a\xa8\x78\x53\xa5" + + "\x8a\x15\x2b\x55\x4a\xfa\xe3\x7b\x85\x8a\x37\x55\x48\xae\x92\x50" + + "\xb4\xc2\xbf\xaa\x41\x17\x1f\xbd\x7b\xf6\xba\xaf\x47\xd1\xa2\x09" + + "\x3d\xba\x75\xeb\xf5\x3f\xc5\xfd\x6f\xbf\xff\x3f\x3d\xfa\xd7\x6d" + + "\x74\x7b\x62\x86\x0c\xff\x79\x9e\x98\x50\x33\xe1\x8f\xb3\x01\xef" + + "\xb6\x38\x7f\x9e\x92\xee\xf9\xa7\xee\xcb\x74\x21\x26\x25\xa1\x6a" + + "\x42\xf6\x73\xff\x96\x4c\x28\x91\x90\xe5\xdc\x79\xa6\x8b\xe2\x52" + + "\xd2\xbf\x5d\x28\x2b\x24\x26\xfc\xa9\xcc\x96\x1e\x97\x31\xfd\xba" + + "\xee\xe9\xde\x3d\x31\xe5\x4f\x65\xc1\xf4\xb8\x0b\x65\x86\x8b\xca"; + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 553677\r\n"); + response.write("Content-Encoding: gzip\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} diff --git a/netwerk/test/unit/test_content_sniffer.js b/netwerk/test/unit/test_content_sniffer.js new file mode 100644 index 0000000000..770eb1922b --- /dev/null +++ b/netwerk/test/unit/test_content_sniffer.js @@ -0,0 +1,163 @@ +// This file tests nsIContentSniffer, introduced in bug 324985 + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const unknownType = "application/x-unknown-content-type"; +const sniffedType = "application/x-sniffed"; + +const snifferCID = Components.ID("{4c93d2db-8a56-48d7-b261-9cf2a8d998eb}"); +const snifferContract = "@mozilla.org/network/unittest/contentsniffer;1"; +const categoryName = "net-content-sniffers"; + +var sniffing_enabled = true; + +var isNosniff = false; + +/** + * This object is both a factory and an nsIContentSniffer implementation (so, it + * is de-facto a service) + */ +var sniffer = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIContentSniffer"]), + createInstance: function sniffer_ci(outer, iid) { + if (outer) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + return this.QueryInterface(iid); + }, + lockFactory: function sniffer_lockf(lock) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + getMIMETypeFromContent(request, data, length) { + return sniffedType; + }, +}; + +var listener = { + onStartRequest: function test_onStartR(request) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + if (chan.contentType == unknownType) { + do_throw("Type should not be unknown!"); + } + if (isNosniff) { + if (chan.contentType == sniffedType) { + do_throw("Sniffer called for X-Content-Type-Options:nosniff"); + } + } else if ( + sniffing_enabled && + this._iteration > 2 && + chan.contentType != sniffedType + ) { + do_throw( + "Expecting <" + + sniffedType + + "> but got <" + + chan.contentType + + "> for " + + chan.URI.spec + ); + } else if (!sniffing_enabled && chan.contentType == sniffedType) { + do_throw( + "Sniffing not enabled but sniffer called for " + chan.URI.spec + ); + } + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + }, + + onStopRequest: function test_onStopR(request, status) { + run_test_iteration(this._iteration); + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan(url) { + var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + if (sniffing_enabled) { + chan.loadFlags |= Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + } + + return chan; +} + +var httpserv = null; +var urls = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/nosniff", nosniffHandler); + httpserv.start(-1); + + urls = [ + // NOTE: First URL here runs without our content sniffer + "data:" + unknownType + ", Some text", + "data:" + unknownType + ", Text", // Make sure sniffing works even if we + // used the unknown content sniffer too + "data:text/plain, Some more text", + "http://localhost:" + httpserv.identity.primaryPort, + "http://localhost:" + httpserv.identity.primaryPort + "/nosniff", + ]; + + Components.manager.nsIComponentRegistrar.registerFactory( + snifferCID, + "Unit test content sniffer", + snifferContract, + sniffer + ); + + run_test_iteration(1); +} + +function nosniffHandler(request, response) { + response.setHeader("X-Content-Type-Options", "nosniff"); +} + +function run_test_iteration(index) { + if (index > urls.length) { + if (sniffing_enabled) { + sniffing_enabled = false; + index = listener._iteration = 1; + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + return; // we're done + } + } + + if (sniffing_enabled && index == 2) { + // Register our sniffer only here + // This also makes sure that dynamic registration is working + var catMan = Cc["@mozilla.org/categorymanager;1"].getService( + Ci.nsICategoryManager + ); + catMan.nsICategoryManager.addCategoryEntry( + categoryName, + "unit test", + snifferContract, + false, + true + ); + } else if (sniffing_enabled && index == 5) { + isNosniff = true; + } + + var chan = makeChan(urls[index - 1]); + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cookie_blacklist.js b/netwerk/test/unit/test_cookie_blacklist.js new file mode 100644 index 0000000000..3f81ae93e9 --- /dev/null +++ b/netwerk/test/unit/test_cookie_blacklist.js @@ -0,0 +1,39 @@ +"use strict"; + +const GOOD_COOKIE = "GoodCookie=OMNOMNOM"; +const SPACEY_COOKIE = "Spacey Cookie=Major Tom"; + +add_task(async () => { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var cookieURI = ios.newURI("http://mozilla.org/test_cookie_blacklist.js"); + const channel = NetUtil.newChannel({ + uri: cookieURI, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + var cookieService = Cc["@mozilla.org/cookieService;1"].getService( + Ci.nsICookieService + ); + cookieService.setCookieStringFromHttp(cookieURI, "BadCookie1=\x01", channel); + cookieService.setCookieStringFromHttp(cookieURI, "BadCookie2=\v", channel); + cookieService.setCookieStringFromHttp( + cookieURI, + "Bad\x07Name=illegal", + channel + ); + cookieService.setCookieStringFromHttp(cookieURI, GOOD_COOKIE, channel); + cookieService.setCookieStringFromHttp(cookieURI, SPACEY_COOKIE, channel); + + CookieXPCShellUtils.createServer({ hosts: ["mozilla.org"] }); + + const storedCookie = await CookieXPCShellUtils.getCookieStringFromDocument( + cookieURI.spec + ); + Assert.equal(storedCookie, GOOD_COOKIE + "; " + SPACEY_COOKIE); +}); diff --git a/netwerk/test/unit/test_cookie_header.js b/netwerk/test/unit/test_cookie_header.js new file mode 100644 index 0000000000..82074196fc --- /dev/null +++ b/netwerk/test/unit/test_cookie_header.js @@ -0,0 +1,113 @@ +// This file tests bug 250375 + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort + "/"; +}); + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +function check_request_header(chan, name, value) { + var chanValue; + try { + chanValue = chan.getRequestHeader(name); + } catch (e) { + do_throw( + "Expected to find header '" + + name + + "' but didn't find it, got exception: " + + e + ); + } + dump("Value for header '" + name + "' is '" + chanValue + "'\n"); + Assert.equal(chanValue, value); +} + +var cookieVal = "C1=V1"; + +var listener = { + onStartRequest: function test_onStartR(request) { + try { + var chan = request.QueryInterface(Ci.nsIHttpChannel); + check_request_header(chan, "Cookie", cookieVal); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); + }, + + onStopRequest: async function test_onStopR(request, status) { + if (this._iteration == 1) { + await run_test_continued(); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan() { + return NetUtil.newChannel({ + uri: URL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var httpserv = null; + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserv = new HttpServer(); + httpserv.start(-1); + + var chan = makeChan(); + + chan.setRequestHeader("Cookie", cookieVal, false); + + chan.asyncOpen(listener); + + do_test_pending(); +} + +async function run_test_continued() { + var chan = makeChan(); + + var cookie2 = "C2=V2"; + + await CookieXPCShellUtils.setCookieToDocument(chan.URI.spec, cookie2); + + chan.setRequestHeader("Cookie", cookieVal, false); + + // We expect that the setRequestHeader overrides the + // automatically-added one, so insert cookie2 in front + cookieVal = cookie2 + "; " + cookieVal; + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_cookie_ipv6.js b/netwerk/test/unit/test_cookie_ipv6.js new file mode 100644 index 0000000000..70bb360e0f --- /dev/null +++ b/netwerk/test/unit/test_cookie_ipv6.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test that channels with different LoadInfo + * are stored in separate namespaces ("cookie jars") + */ + +"use strict"; + +let ip = "[::1]"; +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return `http://${ip}:${httpserver.identity.primaryPort}/`; +}); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +let httpserver = new HttpServer(); + +function cookieSetHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader( + "Set-Cookie", + `Set-Cookie: T1=T2; path=/; SameSite=Lax; domain=${ip}; httponly`, + false + ); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Content-Length", "2"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +add_task(async function test_cookie_ipv6() { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + httpserver.registerPathHandler("/", cookieSetHandler); + httpserver._start(-1, ip); + + var chan = NetUtil.newChannel({ + uri: URL, + loadUsingSystemPrincipal: true, + }); + await new Promise(resolve => { + chan.asyncOpen(new ChannelListener(resolve)); + }); + var cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + equal(cm.cookies.length, 1); +}); diff --git a/netwerk/test/unit/test_cookiejars.js b/netwerk/test/unit/test_cookiejars.js new file mode 100644 index 0000000000..5a421ec3f6 --- /dev/null +++ b/netwerk/test/unit/test_cookiejars.js @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test that channels with different LoadInfo + * are stored in separate namespaces ("cookie jars") + */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); + +var cookieSetPath = "/setcookie"; +var cookieCheckPath = "/checkcookie"; + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +// Test array: +// - element 0: name for cookie, used both to set and later to check +// - element 1: loadInfo (determines cookie namespace) +// +// TODO: bug 722850: make private browsing work per-app, and add tests. For now +// all values are 'false' for PB. + +var tests = [ + { + cookieName: "LCC_App0_BrowF_PrivF", + originAttributes: new OriginAttributes(0, false, 0), + }, + { + cookieName: "LCC_App0_BrowT_PrivF", + originAttributes: new OriginAttributes(0, true, 0), + }, + { + cookieName: "LCC_App1_BrowF_PrivF", + originAttributes: new OriginAttributes(1, false, 0), + }, + { + cookieName: "LCC_App1_BrowT_PrivF", + originAttributes: new OriginAttributes(1, true, 0), + }, +]; + +// test number: index into 'tests' array +var i = 0; + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.loadInfo.originAttributes = tests[i].originAttributes; + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function setCookie() { + var channel = setupChannel(cookieSetPath); + channel.setRequestHeader("foo-set-cookie", tests[i].cookieName, false); + channel.asyncOpen(new ChannelListener(setNextCookie, null)); +} + +function setNextCookie(request, data, context) { + if (++i == tests.length) { + // all cookies set: switch to checking them + i = 0; + checkCookie(); + } else { + info("setNextCookie:i=" + i); + setCookie(); + } +} + +// Open channel that should send one and only one correct Cookie: header to +// server, corresponding to it's namespace +function checkCookie() { + var channel = setupChannel(cookieCheckPath); + channel.asyncOpen(new ChannelListener(completeCheckCookie, null)); +} + +function completeCheckCookie(request, data, context) { + // Look for all cookies in what the server saw: fail if we see any besides the + // one expected cookie for each namespace; + var expectedCookie = tests[i].cookieName; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("foo-saw-cookies"); + + var j; + for (j = 0; j < tests.length; j++) { + var cookieToCheck = tests[j].cookieName; + let found = cookiesSeen.includes(cookieToCheck); + if (found && expectedCookie != cookieToCheck) { + do_throw( + "test index " + + i + + ": found unexpected cookie '" + + cookieToCheck + + "': in '" + + cookiesSeen + + "'" + ); + } else if (!found && expectedCookie == cookieToCheck) { + do_throw( + "test index " + + i + + ": missing expected cookie '" + + expectedCookie + + "': in '" + + cookiesSeen + + "'" + ); + } + } + // If we get here we're good. + info("Saw only correct cookie '" + expectedCookie + "'"); + Assert.ok(true); + + if (++i == tests.length) { + // end of tests + httpserver.stop(do_test_finished); + } else { + checkCookie(); + } +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver.registerPathHandler(cookieSetPath, cookieSetHandler); + httpserver.registerPathHandler(cookieCheckPath, cookieCheckHandler); + httpserver.start(-1); + + setCookie(); + do_test_pending(); +} + +function cookieSetHandler(metadata, response) { + var cookieName = metadata.getHeader("foo-set-cookie"); + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function cookieCheckHandler(metadata, response) { + var cookies = metadata.getHeader("Cookie"); + + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("foo-saw-cookies", cookies, false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} diff --git a/netwerk/test/unit/test_cookiejars_safebrowsing.js b/netwerk/test/unit/test_cookiejars_safebrowsing.js new file mode 100644 index 0000000000..cafecdf7fe --- /dev/null +++ b/netwerk/test/unit/test_cookiejars_safebrowsing.js @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Description of the test: + * We show that we can separate the safebrowsing cookie by creating a custom + * OriginAttributes using a unique safebrowsing first-party domain. Setting this + * custom OriginAttributes on the loadInfo of the channel allows us to query the + * first-party domain and therefore separate the safebrowsing cookie in its own + * cookie-jar. For testing safebrowsing update we do >> NOT << emulate a response + * in the body, rather we only set the cookies in the header of the response + * and confirm that cookies are separated in their own cookie-jar. + * + * 1) We init safebrowsing and simulate an update (cookies are set for localhost) + * + * 2) We open a channel that should send regular cookies, but not the + * safebrowsing cookie. + * + * 3) We open a channel with a custom callback, simulating a safebrowsing cookie + * that should send this simulated safebrowsing cookie as well as the + * real safebrowsing cookies. (Confirming that the safebrowsing cookies + * actually get stored in the correct jar). + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +ChromeUtils.defineModuleGetter( + this, + "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm" +); + +var setCookiePath = "/setcookie"; +var checkCookiePath = "/checkcookie"; +var safebrowsingUpdatePath = "/safebrowsingUpdate"; +var safebrowsingGethashPath = "/safebrowsingGethash"; +var httpserver; + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +function cookieSetHandler(metadata, response) { + var cookieName = metadata.getHeader("set-cookie"); + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function cookieCheckHandler(metadata, response) { + var cookies = metadata.getHeader("Cookie"); + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("saw-cookies", cookies, false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function safebrowsingUpdateHandler(metadata, response) { + var cookieName = "sb-update-cookie"; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write("Ok", "Ok".length); +} + +function safebrowsingGethashHandler(metadata, response) { + var cookieName = "sb-gethash-cookie"; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("set-Cookie", cookieName + "=1; Path=/", false); + response.setHeader("Content-Type", "text/plain"); + + let msg = "test-phish-simplea:1:32\n" + "a".repeat(32); + response.bodyOutputStream.write(msg, msg.length); +} + +function setupChannel(path, originAttributes) { + var channel = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + channel.loadInfo.originAttributes = originAttributes; + channel.QueryInterface(Ci.nsIHttpChannel); + return channel; +} + +function run_test() { + // Set up a profile + do_get_profile(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + httpserver = new HttpServer(); + httpserver.registerPathHandler(setCookiePath, cookieSetHandler); + httpserver.registerPathHandler(checkCookiePath, cookieCheckHandler); + httpserver.registerPathHandler( + safebrowsingUpdatePath, + safebrowsingUpdateHandler + ); + httpserver.registerPathHandler( + safebrowsingGethashPath, + safebrowsingGethashHandler + ); + + httpserver.start(-1); + run_next_test(); +} + +// this test does not emulate a response in the body, +// rather we only set the cookies in the header of response. +add_test(function test_safebrowsing_update() { + var streamUpdater = Cc[ + "@mozilla.org/url-classifier/streamupdater;1" + ].getService(Ci.nsIUrlClassifierStreamUpdater); + + function onSuccess() { + run_next_test(); + } + function onUpdateError() { + do_throw("ERROR: received onUpdateError!"); + } + function onDownloadError() { + do_throw("ERROR: received onDownloadError!"); + } + + streamUpdater.downloadUpdates( + "test-phish-simple,test-malware-simple", + "", + true, + URL + safebrowsingUpdatePath, + onSuccess, + onUpdateError, + onDownloadError + ); +}); + +add_test(function test_safebrowsing_gethash() { + var hashCompleter = Cc[ + "@mozilla.org/url-classifier/hashcompleter;1" + ].getService(Ci.nsIUrlClassifierHashCompleter); + + hashCompleter.complete( + "aaaa", + URL + safebrowsingGethashPath, + "test-phish-simple", + { + completionV2(hash, table, chunkId) {}, + + completionFinished(status) { + Assert.equal(status, Cr.NS_OK); + run_next_test(); + }, + } + ); +}); + +add_test(function test_non_safebrowsing_cookie() { + var cookieName = "regCookie_id0"; + var originAttributes = new OriginAttributes(0, false, 0); + + function setNonSafeBrowsingCookie() { + var channel = setupChannel(setCookiePath, originAttributes); + channel.setRequestHeader("set-cookie", cookieName, false); + channel.asyncOpen(new ChannelListener(checkNonSafeBrowsingCookie, null)); + } + + function checkNonSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen( + new ChannelListener(completeCheckNonSafeBrowsingCookie, null) + ); + } + + function completeCheckNonSafeBrowsingCookie(request, data, context) { + // Confirm that only the >> ONE << cookie is sent over the channel. + var expectedCookie = cookieName + "=1"; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("saw-cookies"); + Assert.equal(cookiesSeen, expectedCookie); + run_next_test(); + } + + setNonSafeBrowsingCookie(); +}); + +add_test(function test_safebrowsing_cookie() { + var cookieName = "sbCookie_id4294967294"; + var originAttributes = new OriginAttributes(0, false, 0); + originAttributes.firstPartyDomain = + "safebrowsing.86868755-6b82-4842-b301-72671a0db32e.mozilla"; + + function setSafeBrowsingCookie() { + var channel = setupChannel(setCookiePath, originAttributes); + channel.setRequestHeader("set-cookie", cookieName, false); + channel.asyncOpen(new ChannelListener(checkSafeBrowsingCookie, null)); + } + + function checkSafeBrowsingCookie() { + var channel = setupChannel(checkCookiePath, originAttributes); + channel.asyncOpen( + new ChannelListener(completeCheckSafeBrowsingCookie, null) + ); + } + + function completeCheckSafeBrowsingCookie(request, data, context) { + // Confirm that all >> THREE << cookies are sent back over the channel: + // a) the safebrowsing cookie set when updating + // b) the safebrowsing cookie set when sending gethash + // c) the regular cookie with custom loadcontext defined in this test. + var expectedCookies = "sb-update-cookie=1; "; + expectedCookies += "sb-gethash-cookie=1; "; + expectedCookies += cookieName + "=1"; + request.QueryInterface(Ci.nsIHttpChannel); + var cookiesSeen = request.getResponseHeader("saw-cookies"); + + Assert.equal(cookiesSeen, expectedCookies); + httpserver.stop(do_test_finished); + } + + setSafeBrowsingCookie(); +}); diff --git a/netwerk/test/unit/test_cookies_async_failure.js b/netwerk/test/unit/test_cookies_async_failure.js new file mode 100644 index 0000000000..c7a455cb53 --- /dev/null +++ b/netwerk/test/unit/test_cookies_async_failure.js @@ -0,0 +1,508 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the various ways opening a cookie database can fail in an asynchronous +// (i.e. after synchronous initialization) manner, and that the database is +// renamed and recreated under each circumstance. These circumstances are, in no +// particular order: +// +// 1) A write operation failing after the database has been read in. +// 2) Asynchronous read failure due to a corrupt database. +// 3) Synchronous read failure due to a corrupt database, when reading: +// a) a single base domain; +// b) the entire database. +// 4) Asynchronous read failure, followed by another failure during INSERT but +// before the database closes for rebuilding. (The additional error should be +// ignored.) +// 5) Asynchronous read failure, followed by an INSERT failure during rebuild. +// This should result in an abort of the database rebuild; the partially- +// built database should be moved to 'cookies.sqlite.bak-rebuild'. + +"use strict"; + +let profile; +let cookie; + +add_task(async () => { + // Set up a profile. + profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // The server. + const hosts = ["foo.com", "hither.com", "haithur.com", "bar.com"]; + for (let i = 0; i < 3000; ++i) { + hosts.push(i + ".com"); + } + CookieXPCShellUtils.createServer({ hosts }); + + // Get the cookie file and the backup file. + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); + + // Create a cookie object for testing. + let now = Date.now() * 1000; + let futureExpiry = Math.round(now / 1e6 + 1000); + cookie = new Cookie( + "oh", + "hai", + "bar.com", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + + await run_test_1(); + await run_test_2(); + await run_test_3(); + await run_test_4(); + await run_test_5(); +}); + +function do_get_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak"); + return file; +} + +function do_get_rebuild_backup_file(profile) { + let file = profile.clone(); + file.append("cookies.sqlite.bak-rebuild"); + return file; +} + +function do_corrupt_db(file) { + // Sanity check: the database size should be larger than 320k, since we've + // written about 460k of data. If it's not, let's make it obvious now. + let size = file.fileSize; + Assert.ok(size > 320e3); + + // Corrupt the database by writing bad data to the end of the file. We + // assume that the important metadata -- table structure etc -- is stored + // elsewhere, and that doing this will not cause synchronous failure when + // initializing the database connection. This is totally empirical -- + // overwriting between 1k and 100k of live data seems to work. (Note that the + // database file will be larger than the actual content requires, since the + // cookie service uses a large growth increment. So we calculate the offset + // based on the expected size of the content, not just the file size.) + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, 2, -1, 0); + let sstream = ostream.QueryInterface(Ci.nsISeekableStream); + let n = size - 320e3 + 20e3; + sstream.seek(Ci.nsISeekableStream.NS_SEEK_SET, size - n); + for (let i = 0; i < n; ++i) { + ostream.write("a", 1); + } + ostream.flush(); + ostream.close(); + + Assert.equal(file.clone().fileSize, size); + return size; +} + +async function run_test_1() { + // Load the profile and populate it. + await CookieXPCShellUtils.setCookieToDocument( + "http://foo.com/", + "oh=hai; max-age=1000" + ); + + // Close the profile. + await promise_close_profile(); + + // Open a database connection now, before we load the profile and begin + // asynchronous write operations. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + Assert.equal(do_count_cookies_in_db(db.db), 1); + + // Load the profile, and wait for async read completion... + await promise_load_profile(); + + // Insert a row. + db.insertCookie(cookie); + db.close(); + + // Attempt to insert a cookie with the same (name, host, path) triplet. + Services.cookiemgr.add( + cookie.host, + cookie.path, + cookie.name, + "hallo", + cookie.isSecure, + cookie.isHttpOnly, + cookie.isSession, + cookie.expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Check that the cookie service accepted the new cookie. + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + + let isRebuildingDone = false; + let rebuildingObserve = function(subject, topic, data) { + isRebuildingDone = true; + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + }; + Services.obs.addObserver(rebuildingObserve, "cookie-db-rebuilding"); + + // Crash test: we're going to rebuild the cookie database. Close all the db + // connections in the main thread and initialize a new database file in the + // cookie thread. Trigger some access of cookies to ensure we won't crash in + // the chaos status. + for (let i = 0; i < 10; ++i) { + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + await new Promise(resolve => executeSoon(resolve)); + } + + // Wait for the cookie service to rename the old database and rebuild if not yet. + if (!isRebuildingDone) { + Services.obs.removeObserver(rebuildingObserve, "cookie-db-rebuilding"); + await new _promise_observer("cookie-db-rebuilding"); + } + + await new Promise(resolve => executeSoon(resolve)); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(cookie.host), 1); + Assert.equal(do_count_cookies(), 2); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed, and that it contains the + // original cookie. + Assert.ok(do_get_backup_file(profile).exists()); + let backupdb = Services.storage.openDatabase(do_get_backup_file(profile)); + Assert.equal(do_count_cookies_in_db(backupdb, "foo.com"), 1); + backupdb.close(); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + + Assert.equal(Services.cookiemgr.countCookiesFromHost("foo.com"), 1); + let cookies = Services.cookiemgr.getCookiesFromHost(cookie.host, {}); + Assert.equal(cookies.length, 1); + let dbcookie = cookies[0]; + Assert.equal(dbcookie.value, "hallo"); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_2() { + // Load the profile and populate it. + do_load_profile(); + + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + db.close(); + + do_load_profile(); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_3() { + // Set the maximum cookies per base domain limit to a large value, so that + // corrupting the database is easier. + Services.prefs.setIntPref("network.cookie.maxPerHost", 3000); + + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://hither.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 10; ++i) { + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + uri = NetUtil.newURI("http://haithur.com/"); + channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 10; i < 3000; ++i) { + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh" + i + "=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("hither.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("haithur.com"), 0); + + // Close the profile. + await promise_close_profile(); + + let db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db, "hither.com"), 0); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Rename it back, and try loading the entire database synchronously. + do_get_backup_file(profile).moveTo(null, "cookies.sqlite"); + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Synchronously read in everything. + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + await promise_close_profile(); + + db = Services.storage.openDatabase(do_get_cookie_file(profile)); + Assert.equal(do_count_cookies_in_db(db), 0); + db.close(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_4() { + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://foo.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + + // Queue up an INSERT for the same base domain. This should also go into + // memory and be written out during database rebuild. + await CookieXPCShellUtils.setCookieToDocument( + "http://0.com/", + "oh2=hai; max-age=1000" + ); + + // At this point, the cookies should still be in memory. + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Check that the original database was renamed. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + // Load the profile, and check that it contains the new cookie. + do_load_profile(); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 1); + Assert.equal(do_count_cookies(), 1); + + // Close the profile. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} + +async function run_test_5() { + // Load the profile and populate it. + do_load_profile(); + Services.cookiesvc.runInTransaction(_ => { + let uri = NetUtil.newURI("http://bar.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; path=/; max-age=1000", + channel + ); + for (let i = 0; i < 3000; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + }); + + // Close the profile. + await promise_close_profile(); + + // Corrupt the database file. + let size = do_corrupt_db(do_get_cookie_file(profile)); + + // Load the profile. + do_load_profile(); + + // At this point, the database connection should be open. Ensure that it + // succeeded. + Assert.ok(!do_get_backup_file(profile).exists()); + + // Recreate a new database since it was corrupted + Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + Assert.ok(!do_get_rebuild_backup_file(profile).exists()); + + // Open a database connection, and write a row that will trigger a constraint + // violation. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + db.insertCookie(cookie); + Assert.equal(do_count_cookies_in_db(db.db, "bar.com"), 1); + Assert.equal(do_count_cookies_in_db(db.db), 1); + db.close(); + + // Check that the original backup and the database itself are gone. + Assert.ok(do_get_backup_file(profile).exists()); + Assert.equal(do_get_backup_file(profile).fileSize, size); + + Assert.equal(Services.cookiemgr.countCookiesFromHost("bar.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("0.com"), 0); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. We do not need to wait for completion, because the + // database has already been closed. Ensure the cookie file is unlocked. + await promise_close_profile(); + + // Clean up. + do_get_cookie_file(profile).remove(false); + do_get_backup_file(profile).remove(false); + Assert.ok(!do_get_cookie_file(profile).exists()); + Assert.ok(!do_get_backup_file(profile).exists()); +} diff --git a/netwerk/test/unit/test_cookies_persistence.js b/netwerk/test/unit/test_cookies_persistence.js new file mode 100644 index 0000000000..a155bb5305 --- /dev/null +++ b/netwerk/test/unit/test_cookies_persistence.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test for cookie persistence across sessions, for the cases: +// 1) network.cookie.lifetimePolicy = 0 (expire naturally) +// 2) network.cookie.lifetimePolicy = 2 (expire at end of session) + +"use strict"; + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + CookieXPCShellUtils.createServer({ + hosts: ["foo.com", "bar.com", "third.com"], + }); + + // Create URIs and channels pointing to foo.com and bar.com. + // We will use these to put foo.com into first and third party contexts. + var spec1 = "http://foo.com/foo.html"; + var spec2 = "http://bar.com/bar.html"; + var uri1 = NetUtil.newURI(spec1); + var uri2 = NetUtil.newURI(spec2); + var channel1 = NetUtil.newChannel({ + uri: uri1, + loadUsingSystemPrincipal: true, + }); + var channel2 = NetUtil.newChannel({ + uri: uri2, + loadUsingSystemPrincipal: true, + }); + + // Force the channel URI to be used when determining the originating URI of + // the channel. + var httpchannel1 = channel1.QueryInterface(Ci.nsIHttpChannelInternal); + var httpchannel2 = channel1.QueryInterface(Ci.nsIHttpChannelInternal); + httpchannel1.forceAllowThirdPartyCookie = true; + httpchannel2.forceAllowThirdPartyCookie = true; + + // test with cookies enabled, and third party cookies persistent. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", false); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + await do_set_cookies(uri1, channel1, false, [1, 2]); + await do_set_cookies(uri2, channel2, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // Again, but don't wait for the async close to complete. This should always + // work, since we blocked on close above and haven't kicked off any writes + // since then. + await promise_close_profile(); + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // test with cookies set to session-only + Services.prefs.setIntPref("network.cookie.lifetimePolicy", 2); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel1, false, [1, 2]); + await do_set_cookies(uri2, channel2, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 0); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); +}); diff --git a/netwerk/test/unit/test_cookies_privatebrowsing.js b/netwerk/test/unit/test_cookies_privatebrowsing.js new file mode 100644 index 0000000000..e594fdf3f9 --- /dev/null +++ b/netwerk/test/unit/test_cookies_privatebrowsing.js @@ -0,0 +1,139 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test private browsing mode. + +"use strict"; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function getCookieStringFromPrivateDocument(uriSpec) { + return CookieXPCShellUtils.getCookieStringFromDocument(uriSpec, { + privateBrowsing: true, + }); +} + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Test with cookies enabled. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + CookieXPCShellUtils.createServer({ hosts: ["foo.com", "bar.com"] }); + + // We need to keep a private-browsing window active, otherwise the + // 'last-pb-context-exited' notification will be dispatched. + const privateBrowsingHolder = await CookieXPCShellUtils.loadContentPage( + "http://bar.com/", + { privateBrowsing: true } + ); + + // Create URIs pointing to foo.com and bar.com. + let uri1 = NetUtil.newURI("http://foo.com/foo.html"); + let uri2 = NetUtil.newURI("http://bar.com/bar.html"); + + // Set a cookie for host 1. + Services.cookiesvc.setCookieStringFromHttp( + uri1, + "oh=hai; max-age=1000", + make_channel(uri1.spec) + ); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1); + + // Enter private browsing mode, set a cookie for host 2, and check the counts. + var chan1 = make_channel(uri1.spec); + chan1.QueryInterface(Ci.nsIPrivateBrowsingChannel); + chan1.setPrivate(true); + + var chan2 = make_channel(uri2.spec); + chan2.QueryInterface(Ci.nsIPrivateBrowsingChannel); + chan2.setPrivate(true); + + Services.cookiesvc.setCookieStringFromHttp( + uri2, + "oh=hai; max-age=1000", + chan2 + ); + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Remove cookies and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + Services.cookiesvc.setCookieStringFromHttp( + uri2, + "oh=hai; max-age=1000", + chan2 + ); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0); + + // Fake a profile change. + await promise_close_profile(); + do_load_profile(); + + // Check that the right cookie persisted. + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0); + + // Enter private browsing mode, set a cookie for host 2, and check the counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + Services.cookiesvc.setCookieStringFromHttp( + uri2, + "oh=hai; max-age=1000", + chan2 + ); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), "oh=hai"); + + // Fake a profile change. + await promise_close_profile(); + do_load_profile(); + + // We're still in private browsing mode, but should have a new session. + // Check counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0); + + // Enter private browsing mode. + + // Fake a profile change, but wait for async read completion. + await promise_close_profile(); + await promise_load_profile(); + + // We're still in private browsing mode, but should have a new session. + // Check counts. + Assert.equal(await getCookieStringFromPrivateDocument(uri1.spec), ""); + Assert.equal(await getCookieStringFromPrivateDocument(uri2.spec), ""); + + // Leave private browsing mode and check counts. + Services.obs.notifyObservers(null, "last-pb-context-exited"); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri1.host), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost(uri2.host), 0); + + // Let's release the last PB window. + privateBrowsingHolder.close(); +}); diff --git a/netwerk/test/unit/test_cookies_profile_close.js b/netwerk/test/unit/test_cookies_profile_close.js new file mode 100644 index 0000000000..f9cff71f4d --- /dev/null +++ b/netwerk/test/unit/test_cookies_profile_close.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the cookie APIs behave sanely after 'profile-before-change'. + +"use strict"; + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Start the cookieservice. + Services.cookies; + + CookieXPCShellUtils.createServer({ hosts: ["foo.com"] }); + + // Set a cookie. + let uri = NetUtil.newURI("http://foo.com"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.scriptSecurityManager.createContentPrincipal(uri, {}); + + await CookieXPCShellUtils.setCookieToDocument( + uri.spec, + "oh=hai; max-age=1000" + ); + + let cookies = Services.cookiemgr.cookies; + Assert.ok(cookies.length == 1); + let cookie = cookies[0]; + + // Fire 'profile-before-change'. + do_close_profile(); + + let promise = new _promise_observer("cookie-db-closed"); + + // Check that the APIs behave appropriately. + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"), + "" + ); + + Assert.equal(Services.cookiesvc.getCookieStringFromHttp(uri, channel), ""); + + await CookieXPCShellUtils.setCookieToDocument(uri.spec, "oh2=hai"); + + Services.cookiesvc.setCookieStringFromHttp(uri, "oh3=hai", channel); + Assert.equal( + await CookieXPCShellUtils.getCookieStringFromDocument("http://foo.com/"), + "" + ); + + do_check_throws(function() { + Services.cookiemgr.removeAll(); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookiemgr.cookies; + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookiemgr.add( + "foo.com", + "", + "oh4", + "hai", + false, + false, + false, + 0, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookiemgr.remove("foo.com", "", "oh4", {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookiemgr.cookieExists(cookie.host, cookie.path, cookie.name, {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookies.countCookiesFromHost("foo.com"); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + do_check_throws(function() { + Services.cookies.getCookiesFromHost("foo.com", {}); + }, Cr.NS_ERROR_NOT_AVAILABLE); + + // Wait for the database to finish closing. + await promise; + + // Load the profile and check that the API is available. + do_load_profile(); + Assert.ok( + Services.cookiemgr.cookieExists(cookie.host, cookie.path, cookie.name, {}) + ); +}); diff --git a/netwerk/test/unit/test_cookies_read.js b/netwerk/test/unit/test_cookies_read.js new file mode 100644 index 0000000000..614862e3f5 --- /dev/null +++ b/netwerk/test/unit/test_cookies_read.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test cookie database asynchronous read operation. + +"use strict"; + +var CMAX = 1000; // # of cookies to create + +add_task(async () => { + // Set up a profile. + let profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Start the cookieservice, to force creation of a database. + // Get the sessionCookies to join the initialization in cookie thread + Services.cookiemgr.sessionCookies; + + // Open a database connection now, after synchronous initialization has + // completed. We may not be able to open one later once asynchronous writing + // begins. + Assert.ok(do_get_cookie_file(profile).exists()); + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 12); + + let uri = NetUtil.newURI("http://foo.com/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + for (let i = 0; i < CMAX; ++i) { + let uri = NetUtil.newURI("http://" + i + ".com/"); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + } + + Assert.equal(do_count_cookies(), CMAX); + + // Wait until all CMAX cookies have been written out to the database. + while (do_count_cookies_in_db(db.db) < CMAX) { + await new Promise(resolve => executeSoon(resolve)); + } + + // Check the WAL file size. We set it to 16 pages of 32k, which means it + // should be around 500k. + let file = db.db.databaseFile; + Assert.ok(file.exists()); + Assert.ok(file.fileSize < 1e6); + db.close(); + + // fake a profile change + await promise_close_profile(); + do_load_profile(); + + // test a few random cookies + Assert.equal(Services.cookiemgr.countCookiesFromHost("999.com"), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost("abc.com"), 0); + Assert.equal(Services.cookiemgr.countCookiesFromHost("100.com"), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost("400.com"), 1); + Assert.equal(Services.cookiemgr.countCookiesFromHost("xyz.com"), 0); + + // force synchronous load of everything + Assert.equal(do_count_cookies(), CMAX); + + // check that everything's precisely correct + for (let i = 0; i < CMAX; ++i) { + let host = i.toString() + ".com"; + Assert.equal(Services.cookiemgr.countCookiesFromHost(host), 1); + } + + // reload again, to make sure the additions were written correctly + await promise_close_profile(); + do_load_profile(); + + // remove some of the cookies, in both reverse and forward order + for (let i = 100; i-- > 0; ) { + let host = i.toString() + ".com"; + Services.cookiemgr.remove(host, "oh", "/", {}); + } + for (let i = CMAX - 100; i < CMAX; ++i) { + let host = i.toString() + ".com"; + Services.cookiemgr.remove(host, "oh", "/", {}); + } + + // check the count + Assert.equal(do_count_cookies(), CMAX - 200); + + // reload again, to make sure the removals were written correctly + await promise_close_profile(); + do_load_profile(); + + // check the count + Assert.equal(do_count_cookies(), CMAX - 200); + + // reload again, but wait for async read completion + await promise_close_profile(); + await promise_load_profile(); + + // check that everything's precisely correct + Assert.equal(do_count_cookies(), CMAX - 200); + for (let i = 100; i < CMAX - 100; ++i) { + let host = i.toString() + ".com"; + Assert.equal(Services.cookiemgr.countCookiesFromHost(host), 1); + } +}); diff --git a/netwerk/test/unit/test_cookies_sync_failure.js b/netwerk/test/unit/test_cookies_sync_failure.js new file mode 100644 index 0000000000..f63955b483 --- /dev/null +++ b/netwerk/test/unit/test_cookies_sync_failure.js @@ -0,0 +1,344 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test the various ways opening a cookie database can fail in a synchronous +// (i.e. immediate) manner, and that the database is renamed and recreated +// under each circumstance. These circumstances are, in no particular order: +// +// 1) A corrupt database, such that opening the connection fails. +// 2) The 'moz_cookies' table doesn't exist. +// 3) Not all of the expected columns exist, and statement creation fails when: +// a) The schema version is larger than the current version. +// b) The schema version is less than or equal to the current version. +// 4) Migration fails. This will have different modes depending on the initial +// version: +// a) Schema 1: the 'lastAccessed' column already exists. +// b) Schema 2: the 'baseDomain' column already exists; or 'baseDomain' +// cannot be computed for a particular host. +// c) Schema 3: the 'creationTime' column already exists; or the +// 'moz_uniqueid' index already exists. + +"use strict"; + +let profile; +let cookieFile; +let backupFile; +let sub_generator; +let now; +let futureExpiry; +let cookie; + +var COOKIE_DATABASE_SCHEMA_CURRENT = 12; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function finish_test() { + executeSoon(function() { + test_generator.return(); + do_test_finished(); + }); +} + +function* do_run_test() { + // Set up a profile. + profile = do_get_profile(); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + // Get the cookie file and the backup file. + cookieFile = profile.clone(); + cookieFile.append("cookies.sqlite"); + backupFile = profile.clone(); + backupFile.append("cookies.sqlite.bak"); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + + // Create a cookie object for testing. + now = Date.now() * 1000; + futureExpiry = Math.round(now / 1e6 + 1000); + cookie = new Cookie( + "oh", + "hai", + "bar.com", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + + sub_generator = run_test_1(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_2(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 99); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, COOKIE_DATABASE_SCHEMA_CURRENT); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 4); + sub_generator.next(); + yield; + + sub_generator = run_test_3(test_generator, 3); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 1, + "ALTER TABLE moz_cookies ADD lastAccessed INTEGER" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 2, + "ALTER TABLE moz_cookies ADD baseDomain TEXT" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_baseDomain(test_generator); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 3, + "ALTER TABLE moz_cookies ADD creationTime INTEGER" + ); + sub_generator.next(); + yield; + + sub_generator = run_test_4_exists( + test_generator, + 3, + "CREATE UNIQUE INDEX moz_uniqueid ON moz_cookies (name, host, path)" + ); + sub_generator.next(); + yield; + + finish_test(); +} + +const garbage = "hello thar!"; + +function create_garbage_file(file) { + // Create an empty database file. + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, -1); + Assert.ok(file.exists()); + Assert.equal(file.fileSize, 0); + + // Write some garbage to it. + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(file, -1, -1, 0); + ostream.write(garbage, garbage.length); + ostream.flush(); + ostream.close(); + + file = file.clone(); // Windows maintains a stat cache. It's lame. + Assert.equal(file.fileSize, garbage.length); +} + +function check_garbage_file(file) { + Assert.ok(file.exists()); + Assert.equal(file.fileSize, garbage.length); + file.remove(false); + Assert.ok(!file.exists()); +} + +function* run_test_1(generator) { + // Create a garbage database file. + create_garbage_file(cookieFile); + + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + // Load the profile and populate it. + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + + // Fake a profile change. + do_close_profile(sub_generator); + yield; + do_load_profile(); + + // Check that the new database contains the cookie, and the old file was + // renamed. + Assert.equal(do_count_cookies(), 1); + check_garbage_file(backupFile); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_2(generator) { + // Load the profile and populate it. + do_load_profile(); + let uri = NetUtil.newURI("http://foo.com/"); + const channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + Services.cookiesvc.setCookieStringFromHttp( + uri, + "oh=hai; max-age=1000", + channel + ); + + // Fake a profile change. + do_close_profile(sub_generator); + yield; + + // Drop the table. + let db = Services.storage.openDatabase(cookieFile); + db.executeSimpleSQL("DROP TABLE moz_cookies"); + db.close(); + + // Load the profile and check that the table is recreated in-place. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + Assert.ok(!backupFile.exists()); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_3(generator, schema) { + // Manually create a schema 2 database, populate it, and set the schema + // version to the desired number. + let schema2db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + schema2db.insertCookie(cookie); + schema2db.db.schemaVersion = schema; + schema2db.close(); + + // Load the profile and check that the column existence test fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset. + let db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + + // Clean up. + cookieFile.remove(false); + Assert.ok(!cookieFile.exists()); + do_run_generator(generator); +} + +function* run_test_4_exists(generator, schema, stmt) { + // Manually create a database, populate it, and add the desired column. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), schema); + db.insertCookie(cookie); + db.db.executeSimpleSQL(stmt); + db.close(); + + // Load the profile and check that migration fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset and the backup file exists. + db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + Assert.ok(backupFile.exists()); + + // Clean up. + cookieFile.remove(false); + backupFile.remove(false); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + do_run_generator(generator); +} + +function* run_test_4_baseDomain(generator) { + // Manually create a database and populate it with a bad host. + let db = new CookieDatabaseConnection(do_get_cookie_file(profile), 2); + let badCookie = new Cookie( + "oh", + "hai", + ".", + "/", + futureExpiry, + now, + now, + false, + false, + false + ); + db.insertCookie(badCookie); + db.close(); + + // Load the profile and check that migration fails. + do_load_profile(); + Assert.equal(do_count_cookies(), 0); + + // Close the profile. + do_close_profile(sub_generator); + yield; + + // Check that the schema version has been reset and the backup file exists. + db = Services.storage.openDatabase(cookieFile); + Assert.equal(db.schemaVersion, COOKIE_DATABASE_SCHEMA_CURRENT); + db.close(); + Assert.ok(backupFile.exists()); + + // Clean up. + cookieFile.remove(false); + backupFile.remove(false); + Assert.ok(!cookieFile.exists()); + Assert.ok(!backupFile.exists()); + do_run_generator(generator); +} diff --git a/netwerk/test/unit/test_cookies_thirdparty.js b/netwerk/test/unit/test_cookies_thirdparty.js new file mode 100644 index 0000000000..413fd1e2e4 --- /dev/null +++ b/netwerk/test/unit/test_cookies_thirdparty.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test third party cookie blocking, for the cases: +// 1) with null channel +// 2) with channel, but with no docshell parent + +"use strict"; + +add_task(async () => { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + Services.prefs.setBoolPref( + "network.cookie.rejectForeignWithExceptions.enabled", + false + ); + + CookieXPCShellUtils.createServer({ + hosts: ["foo.com", "bar.com", "third.com"], + }); + + function createChannel(uri, principal = null) { + const channel = NetUtil.newChannel({ + uri, + loadingPrincipal: + principal || + Services.scriptSecurityManager.createContentPrincipal(uri, {}), + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + return channel.QueryInterface(Ci.nsIHttpChannelInternal); + } + + // Create URIs and channels pointing to foo.com and bar.com. + // We will use these to put foo.com into first and third party contexts. + let spec1 = "http://foo.com/foo.html"; + let spec2 = "http://bar.com/bar.html"; + let uri1 = NetUtil.newURI(spec1); + let uri2 = NetUtil.newURI(spec2); + + // test with cookies enabled + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + + let channel1 = createChannel(uri1); + let channel2 = createChannel(uri2); + + await do_set_cookies(uri1, channel1, true, [1, 2]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel(uri1); + let channel2 = createChannel(uri2); + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked using system principal + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel( + uri1, + Services.scriptSecurityManager.getSystemPrincipal() + ); + let channel2 = createChannel( + uri2, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // Force the channel URI to be used when determining the originating URI of + // the channel. + // test with third party cookies blocked + + // test with cookies enabled + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [1, 2]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } + + // test with third party cookies blocked + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_FOREIGN + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + } + + // test with third party cookies limited + { + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_LIMIT_FOREIGN + ); + + let channel1 = createChannel(uri1); + channel1.forceAllowThirdPartyCookie = true; + + let channel2 = createChannel(uri2); + channel2.forceAllowThirdPartyCookie = true; + + await do_set_cookies(uri1, channel1, true, [0, 1]); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, true, [0, 0]); + Services.cookies.removeAll(); + do_set_single_http_cookie(uri1, channel1, 1); + await do_set_cookies(uri1, channel2, true, [1, 2]); + Services.cookies.removeAll(); + } +}); diff --git a/netwerk/test/unit/test_cookies_thirdparty_session.js b/netwerk/test/unit/test_cookies_thirdparty_session.js new file mode 100644 index 0000000000..a846689790 --- /dev/null +++ b/netwerk/test/unit/test_cookies_thirdparty_session.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// test third party persistence across sessions, for the cases: +// 1) network.cookie.thirdparty.sessionOnly = false +// 2) network.cookie.thirdparty.sessionOnly = true + +"use strict"; + +add_task(async () => { + // Set up a profile. + do_get_profile(); + + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + CookieXPCShellUtils.createServer({ + hosts: ["foo.com", "bar.com", "third.com"], + }); + + // Create URIs and channels pointing to foo.com and bar.com. + // We will use these to put foo.com into first and third party contexts. + var spec1 = "http://foo.com/foo.html"; + var spec2 = "http://bar.com/bar.html"; + var uri1 = NetUtil.newURI(spec1); + var uri2 = NetUtil.newURI(spec2); + var channel1 = NetUtil.newChannel({ + uri: uri1, + loadUsingSystemPrincipal: true, + }); + var channel2 = NetUtil.newChannel({ + uri: uri2, + loadUsingSystemPrincipal: true, + }); + + // Force the channel URI to be used when determining the originating URI of + // the channel. + var httpchannel1 = channel1.QueryInterface(Ci.nsIHttpChannelInternal); + var httpchannel2 = channel2.QueryInterface(Ci.nsIHttpChannelInternal); + httpchannel1.forceAllowThirdPartyCookie = true; + httpchannel2.forceAllowThirdPartyCookie = true; + + // test with cookies enabled, and third party cookies persistent. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", false); + await do_set_cookies(uri1, channel2, false, [1, 2]); + await do_set_cookies(uri2, channel1, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 2); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); + + // test with third party cookies for session only. + Services.prefs.setBoolPref("network.cookie.thirdparty.sessionOnly", true); + Services.cookies.removeAll(); + await do_set_cookies(uri1, channel2, false, [1, 2]); + await do_set_cookies(uri2, channel1, true, [1, 2]); + + // fake a profile change + await promise_close_profile(); + + do_load_profile(); + Assert.equal(Services.cookies.countCookiesFromHost(uri1.host), 0); + Assert.equal(Services.cookies.countCookiesFromHost(uri2.host), 0); +}); diff --git a/netwerk/test/unit/test_cookies_upgrade_10.js b/netwerk/test/unit/test_cookies_upgrade_10.js new file mode 100644 index 0000000000..61dbcda46e --- /dev/null +++ b/netwerk/test/unit/test_cookies_upgrade_10.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function getDBVersion(dbfile) { + let dbConnection = Services.storage.openDatabase(dbfile); + let version = dbConnection.schemaVersion; + dbConnection.close(); + + return version; +} + +function indexExists(dbfile, indexname) { + let dbConnection = Services.storage.openDatabase(dbfile); + let result = dbConnection.indexExists(indexname); + dbConnection.close(); + + return result; +} + +add_task(async function() { + try { + let testfile = do_get_file("data/cookies_v10.sqlite"); + let profileDir = do_get_profile(); + + // Cleanup from any previous tests or failures. + let destFile = profileDir.clone(); + destFile.append("cookies.sqlite"); + if (destFile.exists()) { + destFile.remove(false); + } + + testfile.copyTo(profileDir, "cookies.sqlite"); + Assert.equal(10, getDBVersion(destFile)); + + Assert.ok(destFile.exists()); + + // Check that the index exists + Assert.ok(indexExists(destFile, "moz_basedomain")); + + // Do something that will cause the cookie service access and upgrade the + // database. + Services.cookies.cookies; + + // Pretend that we're about to shut down, to tell the cookie manager + // to clean up its connection with its database. + Services.obs.notifyObservers(null, "profile-before-change"); + + // check for upgraded schema. + Assert.equal(12, getDBVersion(destFile)); + + // Check that the index was deleted + Assert.ok(!indexExists(destFile, "moz_basedomain")); + } catch (e) { + throw new Error(`FAILED: ${e}`); + } +}); diff --git a/netwerk/test/unit/test_data_protocol.js b/netwerk/test/unit/test_data_protocol.js new file mode 100644 index 0000000000..3ce36b2242 --- /dev/null +++ b/netwerk/test/unit/test_data_protocol.js @@ -0,0 +1,91 @@ +/* run some tests on the data: protocol handler */ + +// The behaviour wrt spaces is: +// - Textual content keeps all spaces +// - Other content strips unescaped spaces +// - Base64 content strips escaped and unescaped spaces + +"use strict"; + +var urls = [ + ["data:,", "text/plain", ""], + ["data:,foo", "text/plain", "foo"], + [ + "data:application/octet-stream,foo bar", + "application/octet-stream", + "foobar", + ], + [ + "data:application/octet-stream,foo%20bar", + "application/octet-stream", + "foo bar", + ], + ["data:application/xhtml+xml,foo bar", "application/xhtml+xml", "foo bar"], + ["data:application/xhtml+xml,foo%20bar", "application/xhtml+xml", "foo bar"], + ["data:text/plain,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:text/plain;x=y,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:;x=y,foo%00 bar", "text/plain", "foo\x00 bar"], + ["data:text/plain;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["DATA:TEXT/PLAIN;BASE64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["DaTa:;BaSe64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + ["data:;x=y;base64,Zm9 vI%20GJ%0Dhc%0Ag==", "text/plain", "foo bar"], + // Bug 774240 + [ + "data:application/octet-stream;base64=y,foobar", + "application/octet-stream", + "foobar", + ], + // Bug 781693 + ["data:text/plain;base64;x=y,dGVzdA==", "text/plain", "test"], + ["data:text/plain;x=y;base64,dGVzdA==", "text/plain", "test"], + ["data:text/plain;x=y;base64,", "text/plain", ""], + ["data: ;charset=x ; base64,WA", "text/plain", "X", "x"], + ["data:base64,WA", "text/plain", "WA", "US-ASCII"], +]; + +function run_test() { + dump("*** run_test\n"); + + function on_read_complete(request, data, idx) { + dump("*** run_test.on_read_complete\n"); + + if (request.nsIChannel.contentType != urls[idx][1]) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + urls[idx][1] + + ">" + ); + } + + if (urls[idx][3] && request.nsIChannel.contentCharset !== urls[idx][3]) { + do_throw( + `Charset mismatch! Test <${urls[idx][0]}> - Is <${request.nsIChannel.contentCharset}>, should be <${urls[idx][3]}>` + ); + } + + /* read completed successfully. now compare the data. */ + if (data != urls[idx][2]) { + do_throw( + "Stream contents do not match with direct read! Is <" + + data + + ">, should be <" + + urls[idx][2] + + ">" + ); + } + do_test_finished(); + } + + for (var i = 0; i < urls.length; ++i) { + dump("*** opening channel " + i + "\n"); + do_test_pending(); + var chan = NetUtil.newChannel({ + uri: urls[i][0], + loadUsingSystemPrincipal: true, + }); + chan.contentType = "foo/bar"; // should be ignored + chan.asyncOpen(new ChannelListener(on_read_complete, i)); + } +} diff --git a/netwerk/test/unit/test_defaultURI.js b/netwerk/test/unit/test_defaultURI.js new file mode 100644 index 0000000000..92ac3042b3 --- /dev/null +++ b/netwerk/test/unit/test_defaultURI.js @@ -0,0 +1,185 @@ +"use strict"; + +function stringToDefaultURI(str) { + return Cc["@mozilla.org/network/default-uri-mutator;1"] + .createInstance(Ci.nsIURIMutator) + .setSpec(str) + .finalize(); +} + +add_task(function test_getters() { + let uri = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + equal(uri.spec, "proto://user:password@hostname:123/path/to/file?query#hash"); + equal(uri.prePath, "proto://user:password@hostname:123"); + equal(uri.scheme, "proto"); + equal(uri.userPass, "user:password"); + equal(uri.username, "user"); + equal(uri.password, "password"); + equal(uri.hostPort, "hostname:123"); + equal(uri.host, "hostname"); + equal(uri.port, 123); + equal(uri.pathQueryRef, "/path/to/file?query#hash"); + equal(uri.asciiSpec, uri.spec); + equal(uri.asciiHostPort, uri.hostPort); + equal(uri.asciiHost, uri.host); + equal(uri.ref, "hash"); + equal( + uri.specIgnoringRef, + "proto://user:password@hostname:123/path/to/file?query" + ); + equal(uri.hasRef, true); + equal(uri.filePath, "/path/to/file"); + equal(uri.query, "query"); + equal(uri.displayHost, uri.host); + equal(uri.displayHostPort, uri.hostPort); + equal(uri.displaySpec, uri.spec); + equal(uri.displayPrePath, uri.prePath); +}); + +add_task(function test_methods() { + let uri = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + let uri_same = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query#hash" + ); + let uri_different = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query" + ); + let uri_very_different = stringToDefaultURI( + "proto://user:password@hostname:123/path/to/file?query1#hash" + ); + ok(uri.equals(uri_same)); + ok(!uri.equals(uri_different)); + ok(uri.schemeIs("proto")); + ok(!uri.schemeIs("proto2")); + ok(!uri.schemeIs("proto ")); + ok(!uri.schemeIs("proto\n")); + equal(uri.resolve("/hello"), "proto://user:password@hostname:123/hello"); + equal( + uri.resolve("hello"), + "proto://user:password@hostname:123/path/to/hello" + ); + equal(uri.resolve("proto2:otherhost"), "proto2:otherhost"); + ok(uri.equalsExceptRef(uri_same)); + ok(uri.equalsExceptRef(uri_different)); + ok(!uri.equalsExceptRef(uri_very_different)); +}); + +add_task(function test_mutator() { + let uri = stringToDefaultURI( + "proto://user:pass@host:123/path/to/file?query#hash" + ); + + let check = (callSetters, verify) => { + let m = uri.mutate(); + callSetters(m); + verify(m.finalize()); + }; + + check( + m => m.setSpec("test:bla"), + u => equal(u.spec, "test:bla") + ); + check( + m => m.setSpec("test:bla"), + u => equal(u.spec, "test:bla") + ); + check( + m => m.setScheme("some"), + u => equal(u.spec, "some://user:pass@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass("u"), + u => equal(u.spec, "proto://u@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass("u:p"), + u => equal(u.spec, "proto://u:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass(":p"), + u => equal(u.spec, "proto://:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setUserPass(""), + u => equal(u.spec, "proto://host:123/path/to/file?query#hash") + ); + check( + m => m.setUsername("u"), + u => equal(u.spec, "proto://u:pass@host:123/path/to/file?query#hash") + ); + check( + m => m.setPassword("p"), + u => equal(u.spec, "proto://user:p@host:123/path/to/file?query#hash") + ); + check( + m => m.setHostPort("h"), + u => equal(u.spec, "proto://user:pass@h:123/path/to/file?query#hash") + ); + check( + m => m.setHostPort("h:456"), + u => equal(u.spec, "proto://user:pass@h:456/path/to/file?query#hash") + ); + check( + m => m.setHost("bla"), + u => equal(u.spec, "proto://user:pass@bla:123/path/to/file?query#hash") + ); + check( + m => m.setPort(987), + u => equal(u.spec, "proto://user:pass@host:987/path/to/file?query#hash") + ); + check( + m => m.setPathQueryRef("/p?q#r"), + u => equal(u.spec, "proto://user:pass@host:123/p?q#r") + ); + check( + m => m.setRef("r"), + u => equal(u.spec, "proto://user:pass@host:123/path/to/file?query#r") + ); + check( + m => m.setFilePath("/my/path"), + u => equal(u.spec, "proto://user:pass@host:123/my/path?query#hash") + ); + check( + m => m.setQuery("q"), + u => equal(u.spec, "proto://user:pass@host:123/path/to/file?q#hash") + ); +}); + +add_task(function test_ipv6() { + let uri = stringToDefaultURI("non-special://[2001::1]/"); + equal(uri.hostPort, "[2001::1]"); + // Hopefully this will change after bug 1603199. + equal(uri.host, "2001::1"); +}); + +add_task(function test_serialization() { + let uri = stringToDefaultURI("http://example.org/path"); + let str = serialize_to_escaped_string(uri); + let other = deserialize_from_escaped_string(str).QueryInterface(Ci.nsIURI); + equal(other.spec, uri.spec); +}); + +// This test assumes the serialization never changes, which might not be true. +// It's OK to change the test if we ever make changes to the serialization +// code and this starts failing. +add_task(function test_deserialize_from_string() { + let payload = + "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" + + "%00%C0%00%00%00%00%00%00F%00%00%00%13scheme%3Astuff/to/say"; + equal( + deserialize_from_escaped_string(payload).QueryInterface(Ci.nsIURI).spec, + stringToDefaultURI("scheme:stuff/to/say").spec + ); + + let payload2 = + "%04DZ%A0%FD%27L%99%BDAk%E61%8A%E9%2C%00%00%00%00%00%00%00" + + "%00%C0%00%00%00%00%00%00F%00%00%00%17http%3A//example.org/path"; + equal( + deserialize_from_escaped_string(payload2).QueryInterface(Ci.nsIURI).spec, + stringToDefaultURI("http://example.org/path").spec + ); +}); diff --git a/netwerk/test/unit/test_disabled_ftp.js b/netwerk/test/unit/test_disabled_ftp.js new file mode 100644 index 0000000000..953c9ac45b --- /dev/null +++ b/netwerk/test/unit/test_disabled_ftp.js @@ -0,0 +1,21 @@ +"use strict"; + +registerCleanupFunction(function() { + Services.prefs.clearUserPref("network.ftp.enabled"); +}); + +add_task(async function ftp_disabled() { + Services.prefs.setBoolPref("network.ftp.enabled", false); + Services.prefs.setBoolPref("network.protocol-handler.external.ftp", false); + + Assert.throws( + () => { + NetUtil.newChannel({ + uri: "ftp://ftp.de.debian.org/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + }, + /NS_ERROR_UNKNOWN_PROTOCOL/, + "creating the FTP channel must throw" + ); +}); diff --git a/netwerk/test/unit/test_dns_by_type_resolve.js b/netwerk/test/unit/test_dns_by_type_resolve.js new file mode 100644 index 0000000000..f31325b8c0 --- /dev/null +++ b/netwerk/test/unit/test_dns_by_type_resolve.js @@ -0,0 +1,180 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; +let listen; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.blacklist-duration"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); +}); + +let test_answer = "bXkgdm9pY2UgaXMgbXkgcGFzc3dvcmQ="; +let test_answer_addr = "127.0.0.1"; + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +add_task(async function testTXTResolve() { + // use the h2 server as DOH provider + prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/doh" + ); + + let listenerEsni = new DNSListener(); + let request = dns.asyncResolve( + "_esni.example.com", + dns.RESOLVE_TYPE_TXT, + 0, + null, // resolverInfo + listenerEsni, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listenerEsni; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + let answer = inRecord + .QueryInterface(Ci.nsIDNSTXTRecord) + .getRecordsAsOneString(); + Assert.equal(answer, test_answer, "got correct answer"); +}); + +// verify TXT record pushed on a A record request +add_task(async function testTXTRecordPushPart1() { + prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/txt-dns-push" + ); + let listenerAddr = new DNSListener(); + let request = dns.asyncResolve( + "_esni_push.example.com", + dns.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listenerAddr, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listenerAddr; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.equal(answer, test_answer_addr, "got correct answer"); +}); + +// verify the TXT pushed record +add_task(async function testTXTRecordPushPart2() { + // At this point the second host name should've been pushed and we can resolve it using + // cache only. Set back the URI to a path that fails. + prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/404" + ); + let listenerEsni = new DNSListener(); + let request = dns.asyncResolve( + "_esni_push.example.com", + dns.RESOLVE_TYPE_TXT, + 0, + null, // resolverInfo + listenerEsni, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listenerEsni; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + let answer = inRecord + .QueryInterface(Ci.nsIDNSTXTRecord) + .getRecordsAsOneString(); + Assert.equal(answer, test_answer, "got correct answer"); +}); diff --git a/netwerk/test/unit/test_dns_cancel.js b/netwerk/test/unit/test_dns_cancel.js new file mode 100644 index 0000000000..60cff1cc52 --- /dev/null +++ b/netwerk/test/unit/test_dns_cancel.js @@ -0,0 +1,125 @@ +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); + +var hostname1 = ""; +var hostname2 = ""; +var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +for (var i = 0; i < 20; i++) { + hostname1 += possible.charAt(Math.floor(Math.random() * possible.length)); + hostname2 += possible.charAt(Math.floor(Math.random() * possible.length)); +} + +var requestList1Canceled1; +var requestList1Canceled2; +var requestList1NotCanceled; + +var requestList2Canceled; +var requestList2NotCanceled; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + // One request should be resolved and two request should be canceled. + if (inRequest == requestList1NotCanceled) { + // This request should not be canceled. + Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + // One request should be resolved and the other canceled. + if (inRequest == requestList2NotCanceled) { + // The request should not be canceled. + Assert.notEqual(inStatus, Cr.NS_ERROR_ABORT); + + do_test_finished(); + } + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function run_test() { + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager + ); + var mainThread = threadManager.currentThread; + + var flags = Ci.nsIDNSService.RESOLVE_BYPASS_CACHE; + + // This one will be canceled with cancelAsyncResolve. + requestList1Canceled1 = dns.asyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + dns.cancelAsyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + Cr.NS_ERROR_ABORT, + defaultOriginAttributes + ); + + // This one will not be canceled. + requestList1NotCanceled = dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList1Canceled2 = dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + requestList1Canceled2.cancel(Cr.NS_ERROR_ABORT); + + // This one will not be canceled. + requestList2NotCanceled = dns.asyncResolve( + hostname1, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); + + // This one will be canceled with cancel(Cr.NS_ERROR_ABORT). + requestList2Canceled = dns.asyncResolve( + hostname2, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); + requestList2Canceled.cancel(Cr.NS_ERROR_ABORT); + + do_test_pending(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_dns_disable_ipv4.js b/netwerk/test/unit/test_dns_disable_ipv4.js new file mode 100644 index 0000000000..686bbf9b4a --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv4.js @@ -0,0 +1,55 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV4 flag doesn't +// return any IPv4 addresses. +// + +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +var listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + if (inStatus != Cr.NS_OK) { + Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + do_test_finished(); + return; + } + + while (true) { + try { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + // If there is an answer it should be an IPv6 address + dump(answer); + Assert.ok(answer.includes(":")); + } catch (e) { + break; + } + } + do_test_finished(); + }, +}; + +const defaultOriginAttributes = {}; + +function run_test() { + do_test_pending(); + try { + dns.asyncResolve( + "example.org", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + null, // resolverInfo + listener, + null, + defaultOriginAttributes + ); + } catch (e) { + dump(e); + Assert.ok(false); + do_test_finished(); + } +} diff --git a/netwerk/test/unit/test_dns_disable_ipv6.js b/netwerk/test/unit/test_dns_disable_ipv6.js new file mode 100644 index 0000000000..c22cf57144 --- /dev/null +++ b/netwerk/test/unit/test_dns_disable_ipv6.js @@ -0,0 +1,56 @@ +// +// Tests that calling asyncResolve with the RESOLVE_DISABLE_IPV6 flag doesn't +// return any IPv6 addresses. +// + +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +var listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + if (inStatus != Cr.NS_OK) { + Assert.equal(inStatus, Cr.NS_ERROR_UNKNOWN_HOST); + do_test_finished(); + return; + } + + while (true) { + try { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + // If there is an answer it should be an IPv4 address + dump(answer); + Assert.ok(!answer.includes(":")); + Assert.ok(answer.includes(".")); + } catch (e) { + break; + } + } + do_test_finished(); + }, +}; + +const defaultOriginAttributes = {}; + +function run_test() { + do_test_pending(); + try { + dns.asyncResolve( + "example.com", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + null, // resolverInfo + listener, + null, + defaultOriginAttributes + ); + } catch (e) { + dump(e); + Assert.ok(false); + do_test_finished(); + } +} diff --git a/netwerk/test/unit/test_dns_disabled.js b/netwerk/test/unit/test_dns_disabled.js new file mode 100644 index 0000000000..7b840e2a3b --- /dev/null +++ b/netwerk/test/unit/test_dns_disabled.js @@ -0,0 +1,94 @@ +"use strict"; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const mainThread = threadManager.currentThread; + +function makeListenerBlock(next) { + return { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok(!Components.isSuccessCode(inStatus)); + next(); + }, + }; +} + +function makeListenerDontBlock(next, expectedAnswer) { + return { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + if (expectedAnswer) { + Assert.equal(answer, expectedAnswer); + } else { + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + } + next(); + }, + }; +} + +function do_test({ dnsDisabled, mustBlock, testDomain, expectedAnswer }) { + return new Promise(resolve => { + Services.prefs.setBoolPref("network.dns.disabled", dnsDisabled); + try { + dns.asyncResolve( + testDomain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + mustBlock + ? makeListenerBlock(resolve) + : makeListenerDontBlock(resolve, expectedAnswer), + mainThread, + {} // Default origin attributes + ); + } catch (e) { + Assert.ok(mustBlock === true); + resolve(); + } + }); +} + +function setup() { + override.addIPOverride("foo.bar", "127.0.0.1"); + registerCleanupFunction(function() { + override.clearOverrides(); + Services.prefs.clearUserPref("network.dns.disabled"); + }); +} +setup(); + +// IP literals should be resolved even if dns is disabled +add_task(async function testIPLiteral() { + return do_test({ + dnsDisabled: true, + mustBlock: false, + testDomain: "0x01010101", + expectedAnswer: "1.1.1.1", + }); +}); + +add_task(async function testBlocked() { + return do_test({ + dnsDisabled: true, + mustBlock: true, + testDomain: "foo.bar", + }); +}); + +add_task(async function testNotBlocked() { + return do_test({ + dnsDisabled: false, + mustBlock: false, + testDomain: "foo.bar", + }); +}); diff --git a/netwerk/test/unit/test_dns_localredirect.js b/netwerk/test/unit/test_dns_localredirect.js new file mode 100644 index 0000000000..0af59de813 --- /dev/null +++ b/netwerk/test/unit/test_dns_localredirect.js @@ -0,0 +1,68 @@ +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +var nextTest; + +var listener = { + onLookupComplete(inRequest, inRecord, inStatus) { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + + nextTest(); + do_test_finished(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function run_test() { + prefs.setCharPref("network.dns.localDomains", "local.vingtetun.org"); + + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager + ); + var mainThread = threadManager.currentThread; + nextTest = do_test_2; + dns.asyncResolve( + "local.vingtetun.org", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + do_test_pending(); +} + +function do_test_2() { + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager + ); + var mainThread = threadManager.currentThread; + nextTest = testsDone; + prefs.setCharPref("network.dns.forceResolve", "localhost"); + dns.asyncResolve( + "www.example.com", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.forceResolve"); +} diff --git a/netwerk/test/unit/test_dns_offline.js b/netwerk/test/unit/test_dns_offline.js new file mode 100644 index 0000000000..3b95fe542f --- /dev/null +++ b/netwerk/test/unit/test_dns_offline.js @@ -0,0 +1,113 @@ +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); +var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +var mainThread = threadManager.currentThread; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE); + test2(); + do_test_finished(); + }, +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test3(); + do_test_finished(); + }, +}; + +var listener3 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + cleanup(); + do_test_finished(); + }, +}; + +const defaultOriginAttributes = {}; + +function run_test() { + do_test_pending(); + prefs.setBoolPref("network.dns.offline-localhost", false); + // We always resolve localhost as it's hardcoded without the following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + ioService.offline = true; + try { + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener1, + mainThread, + defaultOriginAttributes + ); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_OFFLINE); + test2(); + do_test_finished(); + } +} + +function test2() { + do_test_pending(); + prefs.setBoolPref("network.dns.offline-localhost", true); + ioService.offline = false; + ioService.offline = true; + // we need to let the main thread run and apply the changes + do_timeout(0, test2Continued); +} + +function test2Continued() { + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener2, + mainThread, + defaultOriginAttributes + ); +} + +function test3() { + do_test_pending(); + ioService.offline = false; + // we need to let the main thread run and apply the changes + do_timeout(0, test3Continued); +} + +function test3Continued() { + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener3, + mainThread, + defaultOriginAttributes + ); +} + +function cleanup() { + prefs.clearUserPref("network.dns.offline-localhost"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); +} diff --git a/netwerk/test/unit/test_dns_onion.js b/netwerk/test/unit/test_dns_onion.js new file mode 100644 index 0000000000..b7824fed3f --- /dev/null +++ b/netwerk/test/unit/test_dns_onion.js @@ -0,0 +1,82 @@ +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +var mainThread = threadManager.currentThread; + +var onionPref; +var localdomainPref; +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +// check that we don't lookup .onion +var listenerBlock = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.ok(!Components.isSuccessCode(inStatus)); + do_test_dontBlock(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +// check that we do lookup .onion (via pref) +var listenerDontBlock = { + onLookupComplete(inRequest, inRecord, inStatus) { + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + all_done(); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), +}; + +const defaultOriginAttributes = {}; + +function do_test_dontBlock() { + prefs.setBoolPref("network.dns.blockDotOnion", false); + dns.asyncResolve( + "private.onion", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listenerDontBlock, + mainThread, + defaultOriginAttributes + ); +} + +function do_test_block() { + prefs.setBoolPref("network.dns.blockDotOnion", true); + try { + dns.asyncResolve( + "private.onion", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listenerBlock, + mainThread, + defaultOriginAttributes + ); + } catch (e) { + // it is ok for this negative test to fail fast + Assert.ok(true); + do_test_dontBlock(); + } +} + +function all_done() { + // reset locally modified prefs + prefs.setCharPref("network.dns.localDomains", localdomainPref); + prefs.setBoolPref("network.dns.blockDotOnion", onionPref); + do_test_finished(); +} + +function run_test() { + onionPref = prefs.getBoolPref("network.dns.blockDotOnion"); + localdomainPref = prefs.getCharPref("network.dns.localDomains"); + prefs.setCharPref("network.dns.localDomains", "private.onion"); + do_test_block(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_dns_originAttributes.js b/netwerk/test/unit/test_dns_originAttributes.js new file mode 100644 index 0000000000..31bc1478a1 --- /dev/null +++ b/netwerk/test/unit/test_dns_originAttributes.js @@ -0,0 +1,99 @@ +"use strict"; + +var dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService); +var threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); +var mainThread = threadManager.currentThread; + +var listener1 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test2(); + do_test_finished(); + }, +}; + +var listener2 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_OK); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + var answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == "127.0.0.1" || answer == "::1"); + test3(); + do_test_finished(); + }, +}; + +var listener3 = { + onLookupComplete(inRequest, inRecord, inStatus) { + Assert.equal(inStatus, Cr.NS_ERROR_OFFLINE); + cleanup(); + do_test_finished(); + }, +}; + +const firstOriginAttributes = { userContextId: 1 }; +const secondOriginAttributes = { userContextId: 2 }; + +// First, we resolve the address normally for first originAttributes. +function run_test() { + do_test_pending(); + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener1, + mainThread, + firstOriginAttributes + ); +} + +// Second, we resolve the same address offline to see whether its DNS cache works +// correctly. +function test2() { + do_test_pending(); + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_OFFLINE, + null, // resolverInfo + listener2, + mainThread, + firstOriginAttributes + ); +} + +// Third, we resolve the same address offline again with different originAttributes. +// This resolving should fail since the DNS cache of the given address is not exist +// for this originAttributes. +function test3() { + do_test_pending(); + try { + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_OFFLINE, + null, // resolverInfo + listener3, + mainThread, + secondOriginAttributes + ); + } catch (e) { + Assert.equal(e.result, Cr.NS_ERROR_OFFLINE); + cleanup(); + do_test_finished(); + } +} + +function cleanup() { + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); +} diff --git a/netwerk/test/unit/test_dns_override.js b/netwerk/test/unit/test_dns_override.js new file mode 100644 index 0000000000..70fa688d8e --- /dev/null +++ b/netwerk/test/unit/test_dns_override.js @@ -0,0 +1,311 @@ +"use strict"; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const defaultOriginAttributes = {}; +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + async firstAddress() { + let all = await this.addresses(); + if (all.length > 0) { + return all[0]; + } + + return undefined; + } + + async addresses() { + let [, inRecord] = await this.promise; + let addresses = []; + if (!inRecord) { + return addresses; // returns [] + } + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + return addresses; + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +const DOMAIN = "example.org"; +const OTHER = "example.com"; + +add_task(async function test_bad_IPs() { + Assert.throws( + () => override.addIPOverride(DOMAIN, DOMAIN), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, ""), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, " "), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); + Assert.throws( + () => override.addIPOverride(DOMAIN, "1-2-3-4"), + /NS_ERROR_UNEXPECTED/, + "Should throw if input is not an IP address" + ); +}); + +add_task(async function test_ipv4() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "1.2.3.4"); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "1.2.3.4"); + + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_ipv6() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "fe80::6a99:9b2b:6ccc:6e1b"); + + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_clearOverrides() { + let listener = new Listener(); + override.addIPOverride(DOMAIN, "1.2.3.4"); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "1.2.3.4"); + + dns.clearCache(false); + override.clearOverrides(); + + listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.notEqual(await listener.firstAddress(), "1.2.3.4"); + + await new Promise(resolve => do_timeout(1000, resolve)); + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_clearHostOverride() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(OTHER, "2.2.2.2"); + override.clearHostOverride(DOMAIN); + let listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + + Assert.notEqual(await listener.firstAddress(), "2.2.2.2"); + + listener = new Listener(); + dns.asyncResolve( + OTHER, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.equal(await listener.firstAddress(), "2.2.2.2"); + + // Note: this test will use the actual system resolver. On windows we do a + // second async call to the system libraries to get the TTL values, which + // keeps the record alive after the onLookupComplete() + // We need to wait for a bit, until the second call is finished before we + // can clear the cache to make sure we evict everything. + // If the next task ever starts failing, with an IP that is not in this + // file, then likely the timeout is too small. + await new Promise(resolve => do_timeout(1000, resolve)); + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_multiple_IPs() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(DOMAIN, "1.1.1.1"); + override.addIPOverride(DOMAIN, "::1"); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + let listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), [ + "2.2.2.2", + "1.1.1.1", + "::1", + "fe80::6a99:9b2b:6ccc:6e1b", + ]); + + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_address_family_flags() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.addIPOverride(DOMAIN, "1.1.1.1"); + override.addIPOverride(DOMAIN, "::1"); + override.addIPOverride(DOMAIN, "fe80::6a99:9b2b:6ccc:6e1b"); + let listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), [ + "::1", + "fe80::6a99:9b2b:6ccc:6e1b", + ]); + + listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + null, + listener, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual(await listener.addresses(), ["2.2.2.2", "1.1.1.1"]); + + dns.clearCache(false); + override.clearOverrides(); +}); + +add_task(async function test_cname_flag() { + override.addIPOverride(DOMAIN, "2.2.2.2"); + let listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.throws( + () => inRecord.canonicalName, + /NS_ERROR_NOT_AVAILABLE/, + "No canonical name flag" + ); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, + listener, + mainThread, + defaultOriginAttributes + ); + [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, DOMAIN, "No canonical name specified"); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + dns.clearCache(false); + override.clearOverrides(); + + override.addIPOverride(DOMAIN, "2.2.2.2"); + override.setCnameOverride(DOMAIN, OTHER); + listener = new Listener(); + dns.asyncResolve( + DOMAIN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, + listener, + mainThread, + defaultOriginAttributes + ); + [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, OTHER, "Must have correct CNAME"); + Assert.equal(inRecord.getNextAddrAsString(), "2.2.2.2"); + + dns.clearCache(false); + override.clearOverrides(); +}); diff --git a/netwerk/test/unit/test_dns_override_for_localhost.js b/netwerk/test/unit/test_dns_override_for_localhost.js new file mode 100644 index 0000000000..462cb90aac --- /dev/null +++ b/netwerk/test/unit/test_dns_override_for_localhost.js @@ -0,0 +1,92 @@ +"use strict"; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const override = Cc["@mozilla.org/network/native-dns-override;1"].getService( + Ci.nsINativeDNSResolverOverride +); +const defaultOriginAttributes = {}; +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + async addresses() { + let [, inRecord] = await this.promise; + let addresses = []; + if (!inRecord) { + return addresses; // returns [] + } + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + return addresses; + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +["localhost", "vhost.localhost"].forEach(domain => { + add_task(async function test_() { + let listener1 = new Listener(); + const overrides = ["1.2.3.4", "5.6.7.8"]; + overrides.forEach(ip_address => { + override.addIPOverride(domain, ip_address); + }); + + // Verify that loopback host names are not overridden. + dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener1, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual( + await listener1.addresses(), + ["127.0.0.1", "::1"], + `${domain} is not overridden` + ); + + // Verify that if localhost hijacking is enabled, the overrides + // registered above are taken into account. + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + let listener2 = new Listener(); + dns.asyncResolve( + domain, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, + listener2, + mainThread, + defaultOriginAttributes + ); + Assert.deepEqual( + await listener2.addresses(), + overrides, + `${domain} is overridden` + ); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + + dns.clearCache(false); + override.clearOverrides(); + }); +}); diff --git a/netwerk/test/unit/test_dns_proxy_bypass.js b/netwerk/test/unit/test_dns_proxy_bypass.js new file mode 100644 index 0000000000..8fbe91c346 --- /dev/null +++ b/netwerk/test/unit/test_dns_proxy_bypass.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +var prefs = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +var url = "ws://dnsleak.example.com"; + +var dnsRequestObserver = { + register() { + this.obs = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + this.obs.addObserver(this, "dns-resolution-request"); + }, + + unregister() { + if (this.obs) { + this.obs.removeObserver(this, "dns-resolution-request"); + } + }, + + observe(subject, topic, data) { + if (topic == "dns-resolution-request") { + info(data); + if (data.indexOf("dnsleak.example.com") > -1) { + try { + Assert.ok(false); + } catch (e) {} + } + } + }, +}; + +var listener = { + onAcknowledge(aContext, aSize) {}, + onBinaryMessageAvailable(aContext, aMsg) {}, + onMessageAvailable(aContext, aMsg) {}, + onServerClose(aContext, aCode, aReason) {}, + onStart(aContext) {}, + onStop(aContext, aStatusCode) { + prefs.clearUserPref("network.proxy.socks"); + prefs.clearUserPref("network.proxy.socks_port"); + prefs.clearUserPref("network.proxy.type"); + prefs.clearUserPref("network.proxy.socks_remote_dns"); + prefs.clearUserPref("network.dns.notifyResolution"); + dnsRequestObserver.unregister(); + do_test_finished(); + }, +}; + +function run_test() { + dnsRequestObserver.register(); + prefs.setBoolPref("network.dns.notifyResolution", true); + prefs.setCharPref("network.proxy.socks", "127.0.0.1"); + prefs.setIntPref("network.proxy.socks_port", 9000); + prefs.setIntPref("network.proxy.type", 1); + prefs.setBoolPref("network.proxy.socks_remote_dns", true); + var chan = Cc["@mozilla.org/network/protocol;1?name=ws"].createInstance( + Ci.nsIWebSocketChannel + ); + + chan.initLoadInfo( + null, // aLoadingNode + Services.scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_WEBSOCKET + ); + + var uri = ioService.newURI(url); + chan.asyncOpen(uri, url, 0, listener, null); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_dns_service.js b/netwerk/test/unit/test_dns_service.js new file mode 100644 index 0000000000..5ee3ef2f0f --- /dev/null +++ b/netwerk/test/unit/test_dns_service.js @@ -0,0 +1,68 @@ +"use strict"; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const defaultOriginAttributes = {}; +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +class Listener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +Listener.prototype.QueryInterface = ChromeUtils.generateQI(["nsIDNSListener"]); + +const DOMAIN_IDN = "bücher.org"; +const ACE_IDN = "xn--bcher-kva.org"; + +const DOMAIN = "localhost"; +const ADDR1 = "127.0.0.1"; +const ADDR2 = "::1"; + +add_task(async function test_dns_localhost() { + let listener = new Listener(); + dns.asyncResolve( + "localhost", + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let answer = inRecord.getNextAddrAsString(); + Assert.ok(answer == ADDR1 || answer == ADDR2); +}); + +add_task(async function test_idn_cname() { + let listener = new Listener(); + dns.asyncResolve( + DOMAIN_IDN, + Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT, + Ci.nsIDNSService.RESOLVE_CANONICAL_NAME, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + let [, inRecord] = await listener; + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + Assert.equal(inRecord.canonicalName, ACE_IDN, "IDN is returned as punycode"); +}); diff --git a/netwerk/test/unit/test_domain_eviction.js b/netwerk/test/unit/test_domain_eviction.js new file mode 100644 index 0000000000..232fc89d99 --- /dev/null +++ b/netwerk/test/unit/test_domain_eviction.js @@ -0,0 +1,185 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that domain eviction occurs when the cookies per base domain limit is +// reached, and that expired cookies are evicted before live cookies. + +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function continue_test() { + do_run_generator(test_generator); +} + +function* do_run_test() { + // Set quotaPerHost to maxPerHost - 1, so there is only one cookie + // will be evicted everytime. + Services.prefs.setIntPref("network.cookie.quotaPerHost", 49); + // Set the base domain limit to 50 so we have a known value. + Services.prefs.setIntPref("network.cookie.maxPerHost", 50); + + let futureExpiry = Math.floor(Date.now() / 1000 + 1000); + + // test eviction under the 50 cookies per base domain limit. this means + // that cookies for foo.com and bar.foo.com should count toward this limit, + // while cookies for baz.com should not. there are several tests we perform + // to make sure the base domain logic is working correctly. + + // 1) simplest case: set 100 cookies for "foo.bar" and make sure 50 survive. + setCookies("foo.bar", 100, futureExpiry); + Assert.equal(countCookies("foo.bar", "foo.bar"), 50); + + // 2) set cookies for different subdomains of "foo.baz", and an unrelated + // domain, and make sure all 50 within the "foo.baz" base domain are counted. + setCookies("foo.baz", 10, futureExpiry); + setCookies(".foo.baz", 10, futureExpiry); + setCookies("bar.foo.baz", 10, futureExpiry); + setCookies("baz.bar.foo.baz", 10, futureExpiry); + setCookies("unrelated.domain", 50, futureExpiry); + Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 40); + setCookies("foo.baz", 20, futureExpiry); + Assert.equal(countCookies("foo.baz", "baz.bar.foo.baz"), 50); + + // 3) ensure cookies are evicted by order of lastAccessed time, if the + // limit on cookies per base domain is reached. + setCookies("horse.radish", 10, futureExpiry); + + // Wait a while, to make sure the first batch of cookies is older than + // the second (timer resolution varies on different platforms). + do_timeout(100, continue_test); + yield; + + setCookies("tasty.horse.radish", 50, futureExpiry); + Assert.equal(countCookies("horse.radish", "horse.radish"), 50); + + for (let cookie of Services.cookiemgr.cookies) { + if (cookie.host == "horse.radish") { + do_throw("cookies not evicted by lastAccessed order"); + } + } + + // Test that expired cookies for a domain are evicted before live ones. + let shortExpiry = Math.floor(Date.now() / 1000 + 2); + setCookies("captchart.com", 49, futureExpiry); + Services.cookiemgr.add( + "captchart.com", + "", + "test100", + "eviction", + false, + false, + false, + shortExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + do_timeout(2100, continue_test); + yield; + + Assert.equal(countCookies("captchart.com", "captchart.com"), 50); + Services.cookiemgr.add( + "captchart.com", + "", + "test200", + "eviction", + false, + false, + false, + futureExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + Assert.equal(countCookies("captchart.com", "captchart.com"), 50); + + for (let cookie of Services.cookiemgr.getCookiesFromHost( + "captchart.com", + {} + )) { + Assert.ok(cookie.expiry == futureExpiry); + } + + do_finish_generator_test(test_generator); +} + +// set 'aNumber' cookies with host 'aHost', with distinct names. +function setCookies(aHost, aNumber, aExpiry) { + for (let i = 0; i < aNumber; ++i) { + Services.cookiemgr.add( + aHost, + "", + "test" + i, + "eviction", + false, + false, + false, + aExpiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + } +} + +// count how many cookies are within domain 'aBaseDomain', using three +// independent interface methods on nsICookieManager: +// 1) 'cookies', an array of all cookies; +// 2) 'countCookiesFromHost', which returns the number of cookies within the +// base domain of 'aHost', +// 3) 'getCookiesFromHost', which returns an array of 2). +function countCookies(aBaseDomain, aHost) { + // count how many cookies are within domain 'aBaseDomain' using the cookies + // array. + let cookies = []; + for (let cookie of Services.cookiemgr.cookies) { + if ( + cookie.host.length >= aBaseDomain.length && + cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain + ) { + cookies.push(cookie); + } + } + + // confirm the count using countCookiesFromHost and getCookiesFromHost. + let result = cookies.length; + Assert.equal( + Services.cookiemgr.countCookiesFromHost(aBaseDomain), + cookies.length + ); + Assert.equal(Services.cookiemgr.countCookiesFromHost(aHost), cookies.length); + + for (let cookie of Services.cookiemgr.getCookiesFromHost(aHost, {})) { + if ( + cookie.host.length >= aBaseDomain.length && + cookie.host.slice(cookie.host.length - aBaseDomain.length) == aBaseDomain + ) { + let found = false; + for (let i = 0; i < cookies.length; ++i) { + if (cookies[i].host == cookie.host && cookies[i].name == cookie.name) { + found = true; + cookies.splice(i, 1); + break; + } + } + + if (!found) { + do_throw("cookie " + cookie.name + " not found in master cookies"); + } + } else { + do_throw( + "cookie host " + cookie.host + " not within domain " + aBaseDomain + ); + } + } + + Assert.equal(cookies.length, 0); + + return result; +} diff --git a/netwerk/test/unit/test_doomentry.js b/netwerk/test/unit/test_doomentry.js new file mode 100644 index 0000000000..23928d223d --- /dev/null +++ b/netwerk/test/unit/test_doomentry.js @@ -0,0 +1,108 @@ +/** + * Test for nsICacheStorage.asyncDoomURI(). + * It tests dooming + * - an existent inactive entry + * - a non-existent inactive entry + * - an existent active entry + */ + +"use strict"; + +function doom(url, callback) { + get_cache_service() + .diskCacheStorage(Services.loadContextInfo.default, false) + .asyncDoomURI(createURI(url), "", { + onCacheEntryDoomed(result) { + callback(result); + }, + }); +} + +function write_and_check(str, data, len) { + var written = str.write(data, len); + if (written != len) { + do_throw( + "str.write has not written all data!\n" + + " Expected: " + + len + + "\n" + + " Actual: " + + written + + "\n" + ); + } +} + +function write_entry() { + asyncOpenCacheEntry( + "http://testentry/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + function(status, entry) { + write_entry_cont(entry, entry.openOutputStream(0, -1)); + } + ); +} + +function write_entry_cont(entry, ostream) { + var data = "testdata"; + write_and_check(ostream, data, data.length); + ostream.close(); + entry.close(); + doom("http://testentry/", check_doom1); +} + +function check_doom1(status) { + Assert.equal(status, Cr.NS_OK); + doom("http://nonexistententry/", check_doom2); +} + +function check_doom2(status) { + Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE); + asyncOpenCacheEntry( + "http://testentry/", + "disk", + Ci.nsICacheStorage.OPEN_TRUNCATE, + null, + function(status, entry) { + write_entry2(entry, entry.openOutputStream(0, -1)); + } + ); +} + +var gEntry; +var gOstream; +function write_entry2(entry, ostream) { + // write some data and doom the entry while it is active + var data = "testdata"; + write_and_check(ostream, data, data.length); + gEntry = entry; + gOstream = ostream; + doom("http://testentry/", check_doom3); +} + +function check_doom3(status) { + Assert.equal(status, Cr.NS_OK); + // entry was doomed but writing should still succeed + var data = "testdata"; + write_and_check(gOstream, data, data.length); + gOstream.close(); + gEntry.close(); + // dooming the same entry again should fail + doom("http://testentry/", check_doom4); +} + +function check_doom4(status) { + Assert.equal(status, Cr.NS_ERROR_NOT_AVAILABLE); + do_test_finished(); +} + +function run_test() { + do_get_profile(); + + // clear the cache + evict_cache_entries(); + write_entry(); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_duplicate_headers.js b/netwerk/test/unit/test_duplicate_headers.js new file mode 100644 index 0000000000..0c747c6066 --- /dev/null +++ b/netwerk/test/unit/test_duplicate_headers.js @@ -0,0 +1,558 @@ +/* + * Tests bugs 597706, 655389: prevent duplicate headers with differing values + * for some headers like Content-Length, Location, etc. + */ + +//////////////////////////////////////////////////////////////////////////////// +// Test infrastructure + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var test_flags = []; +var testPathBase = "/dupe_hdrs"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(1); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, globalThis["handler" + num]); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(globalThis["completeTest" + num], channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: FAIL because of conflicting Content-Length headers +test_flags[1] = CL_EXPECT_FAILURE; + +function handler1(metadata, response) { + var body = "012345678901234567890123456789"; + // Comrades! We must seize power from the petty-bourgeois running dogs of + // httpd.js in order to reply with multiple instances of the same header! + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: 20\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest1(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(2); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: OK to have duplicate same Content-Length headers + +function handler2(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest2(request, data, ctx) { + Assert.equal(request.status, 0); + run_test_number(3); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: FAIL: 2nd Content-length is blank +test_flags[3] = CL_EXPECT_FAILURE; + +function handler3(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length:\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest3(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(4); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: ensure that blank C-len header doesn't allow attacker to reset Clen, +// then insert CRLF attack +test_flags[4] = CL_EXPECT_FAILURE; + +function handler4(metadata, response) { + var body = "012345678901234567890123456789"; + + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + + // Bad Mr Hacker! Bad! + var evilBody = "We are the Evil bytes, Evil bytes, Evil bytes!"; + response.write("Content-Length:\r\n"); + response.write("Content-Length: %s\r\n\r\n%s" % (evilBody.length, evilBody)); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest4(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(5); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: ensure that we take 1st instance of duplicate, nonmerged headers that +// are permitted : (ex: Referrer) + +function handler5(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Referer: naive.org\r\n"); + response.write("Referer: evil.net\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest5(request, data, ctx) { + try { + let referer = request.getResponseHeader("Referer"); + Assert.equal(referer, "naive.org"); + } catch (ex) { + do_throw("Referer header should be present"); + } + + run_test_number(6); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 5: FAIL if multiple, different Location: headers present +// - needed to prevent CRLF injection attacks +test_flags[6] = CL_EXPECT_FAILURE; + +function handler6(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Location: " + URL + "/content\r\n"); + response.write("Location: http://www.microsoft.com/\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest6(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + // run_test_number(7); // Test 7 leaking under e10s: unrelated bug? + run_test_number(8); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 7: OK to have multiple Location: headers with same value + +function handler7(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 5 + response.write("Location: " + URL + testPathBase + "5\r\n"); + response.write("Location: " + URL + testPathBase + "5\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest7(request, data, ctx) { + // for some reason need this here + request.QueryInterface(Ci.nsIHttpChannel); + + try { + let referer = request.getResponseHeader("Referer"); + Assert.equal(referer, "naive.org"); + } catch (ex) { + do_throw("Referer header should be present"); + } + + run_test_number(8); +} + +//////////////////////////////////////////////////////////////////////////////// +// FAIL if 2nd Location: headers blank +test_flags[8] = CL_EXPECT_FAILURE; + +function handler8(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Location:\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest8(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(9); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 9: ensure that blank Location header doesn't allow attacker to reset, +// then insert an evil one +test_flags[9] = CL_EXPECT_FAILURE; + +function handler9(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 2 + response.write("Location: " + URL + testPathBase + "2\r\n"); + response.write("Location:\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest9(request, data, ctx) { + // All redirection should fail: + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(10); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 10: FAIL: if conflicting values for Content-Dispo +test_flags[10] = CL_EXPECT_FAILURE; + +function handler10(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("Content-Disposition: attachment; filename=bar\r\n"); + response.write("Content-Disposition: attachment; filename=baz\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest10(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(11); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 11: OK to have duplicate same Content-Disposition headers + +function handler11(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("Content-Disposition: attachment; filename=foo\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest11(request, data, ctx) { + Assert.equal(request.status, 0); + + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionFilename, "foo"); + Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); + } catch (ex) { + do_throw("error parsing Content-Disposition: " + ex); + } + + run_test_number(12); +} + +//////////////////////////////////////////////////////////////////////////////// +// Bug 716801 OK for Location: header to be blank + +function handler12(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Location:\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest12(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(13); +} + +//////////////////////////////////////////////////////////////////////////////// +// Negative content length is ok +test_flags[13] = CL_ALLOW_UNKNOWN_CL; + +function handler13(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest13(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(14); +} + +//////////////////////////////////////////////////////////////////////////////// +// leading negative content length is not ok if paired with positive one + +test_flags[14] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler14(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest14(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(15); +} + +//////////////////////////////////////////////////////////////////////////////// +// trailing negative content length is not ok if paired with positive one + +test_flags[15] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler15(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Content-Length: -1\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest15(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(16); +} + +//////////////////////////////////////////////////////////////////////////////// +// empty content length is ok +test_flags[16] = CL_ALLOW_UNKNOWN_CL; +let reran16 = false; + +function handler16(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: \r\n"); + response.write("Cache-Control: max-age=600\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest16(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + if (!reran16) { + reran16 = true; + run_test_number(16); + } else { + run_test_number(17); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// empty content length paired with non empty is not ok +test_flags[17] = CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL; + +function handler17(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: \r\n"); + response.write("Content-Length: 30\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest17(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + run_test_number(18); +} + +//////////////////////////////////////////////////////////////////////////////// +// alpha content-length is just like -1 +test_flags[18] = CL_ALLOW_UNKNOWN_CL; + +function handler18(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: seventeen\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest18(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(19); +} + +//////////////////////////////////////////////////////////////////////////////// +// semi-colons are ok too in the content-length +test_flags[19] = CL_ALLOW_UNKNOWN_CL; + +function handler19(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 200 OK\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30;\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest19(request, data, ctx) { + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(30, data.length); + + run_test_number(20); +} + +//////////////////////////////////////////////////////////////////////////////// +// FAIL if 1st Location: header is blank, followed by non-blank +test_flags[20] = CL_EXPECT_FAILURE; + +function handler20(metadata, response) { + var body = "012345678901234567890123456789"; + response.seizePower(); + response.write("HTTP/1.0 301 Moved\r\n"); + response.write("Content-Type: text/plain\r\n"); + response.write("Content-Length: 30\r\n"); + // redirect to previous test handler that completes OK: test 4 + response.write("Location:\r\n"); + response.write("Location: " + URL + testPathBase + "4\r\n"); + response.write("Connection: close\r\n"); + response.write("\r\n"); + response.write(body); + response.finish(); +} + +function completeTest20(request, data, ctx) { + Assert.equal(request.status, Cr.NS_ERROR_CORRUPTED_CONTENT); + + endTests(); +} diff --git a/netwerk/test/unit/test_event_sink.js b/netwerk/test/unit/test_event_sink.js new file mode 100644 index 0000000000..ecb16d845d --- /dev/null +++ b/netwerk/test/unit/test_event_sink.js @@ -0,0 +1,195 @@ +// This file tests channel event sinks (bug 315598 et al) + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +const sinkCID = Components.ID("{14aa4b81-e266-45cb-88f8-89595dece114}"); +const sinkContract = "@mozilla.org/network/unittest/channeleventsink;1"; + +const categoryName = "net-channel-event-sinks"; + +/** + * This object is both a factory and an nsIChannelEventSink implementation (so, it + * is de-facto a service). It's also an interface requestor that gives out + * itself when asked for nsIChannelEventSink. + */ +var eventsink = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIChannelEventSink"]), + createInstance: function eventsink_ci(outer, iid) { + if (outer) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + return this.QueryInterface(iid); + }, + lockFactory: function eventsink_lockf(lock) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + asyncOnChannelRedirect: function eventsink_onredir( + oldChan, + newChan, + flags, + callback + ) { + // veto + this.called = true; + throw Components.Exception("", Cr.NS_BINDING_ABORTED); + }, + + getInterface: function eventsink_gi(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + called: false, +}; + +var listener = { + expectSinkCall: true, + + onStartRequest: function test_onStartR(request) { + try { + // Commenting out this check pending resolution of bug 255119 + //if (Components.isSuccessCode(request.status)) + // do_throw("Channel should have a failure code!"); + + // The current URI must be the original URI, as all redirects have been + // cancelled + if ( + !(request instanceof Ci.nsIChannel) || + !request.URI.equals(request.originalURI) + ) { + do_throw( + "Wrong URI: Is <" + + request.URI.spec + + ">, should be <" + + request.originalURI.spec + + ">" + ); + } + + if (request instanceof Ci.nsIHttpChannel) { + // As we expect a blocked redirect, verify that we have a 3xx status + Assert.equal(Math.floor(request.responseStatus / 100), 3); + Assert.equal(request.requestSucceeded, false); + } + + Assert.equal(eventsink.called, this.expectSinkCall); + } catch (e) { + do_throw("Unexpected exception: " + e); + } + + throw Components.Exception("", Cr.NS_ERROR_ABORT); + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + if (this._iteration <= 2) { + run_test_continued(); + } else { + do_test_pending(); + httpserv.stop(do_test_finished); + } + do_test_finished(); + }, + + _iteration: 1, +}; + +function makeChan(url) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/redirect", redirect); + httpserv.registerPathHandler("/redirectfile", redirectfile); + httpserv.start(-1); + + Components.manager.nsIComponentRegistrar.registerFactory( + sinkCID, + "Unit test Event sink", + sinkContract, + eventsink + ); + + // Step 1: Set the callbacks on the listener itself + var chan = makeChan(URL + "/redirect"); + chan.notificationCallbacks = eventsink; + + chan.asyncOpen(listener); + + do_test_pending(); +} + +function run_test_continued() { + eventsink.called = false; + + var catMan = Cc["@mozilla.org/categorymanager;1"].getService( + Ci.nsICategoryManager + ); + + var chan; + if (listener._iteration == 1) { + // Step 2: Category entry + catMan.nsICategoryManager.addCategoryEntry( + categoryName, + "unit test", + sinkContract, + false, + true + ); + chan = makeChan(URL + "/redirect"); + } else { + // Step 3: Global contract id + catMan.nsICategoryManager.deleteCategoryEntry( + categoryName, + "unit test", + false + ); + listener.expectSinkCall = false; + chan = makeChan(URL + "/redirectfile"); + } + + listener._iteration++; + chan.asyncOpen(listener); + + do_test_pending(); +} + +// PATHS + +// /redirect +function redirect(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader( + "Location", + "http://localhost:" + metadata.port + "/", + false + ); + + var body = "Moved\n"; + response.bodyOutputStream.write(body, body.length); +} + +// /redirectfile +function redirectfile(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved Permanently"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Location", "file:///etc/", false); + + var body = "Attempted to move to a file URI, but failed.\n"; + response.bodyOutputStream.write(body, body.length); +} diff --git a/netwerk/test/unit/test_eviction.js b/netwerk/test/unit/test_eviction.js new file mode 100644 index 0000000000..43be39dba2 --- /dev/null +++ b/netwerk/test/unit/test_eviction.js @@ -0,0 +1,252 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var test_generator = do_run_test(); + +function run_test() { + do_test_pending(); + do_run_generator(test_generator); +} + +function continue_test() { + do_run_generator(test_generator); +} + +function repeat_test() { + // The test is probably going to fail because setting a batch of cookies took + // a significant fraction of 'gPurgeAge'. Compensate by rerunning the + // test with a larger purge age. + Assert.ok(gPurgeAge < 64); + gPurgeAge *= 2; + gShortExpiry *= 2; + + executeSoon(function() { + test_generator.return(); + test_generator = do_run_test(); + do_run_generator(test_generator); + }); +} + +// Purge threshold, in seconds. +var gPurgeAge = 1; + +// Short expiry age, in seconds. +var gShortExpiry = 2; + +// Required delay to ensure a purge occurs, in milliseconds. This must be at +// least gPurgeAge + 10%, and includes a little fuzz to account for timer +// resolution and possible differences between PR_Now() and Date.now(). +function get_purge_delay() { + return gPurgeAge * 1100 + 100; +} + +// Required delay to ensure a cookie set with an expiry time 'gShortExpiry' into +// the future will have expired. +function get_expiry_delay() { + return gShortExpiry * 1000 + 100; +} + +function* do_run_test() { + // Set up a profile. + do_get_profile(); + + // twiddle prefs to convenient values for this test + Services.prefs.setIntPref("network.cookie.purgeAge", gPurgeAge); + Services.prefs.setIntPref("network.cookie.maxNumber", 100); + + let expiry = Date.now() / 1000 + 1000; + + // eviction is performed based on two limits: when the total number of cookies + // exceeds maxNumber + 10% (110), and when cookies are older than purgeAge + // (1 second). purging is done when both conditions are satisfied, and only + // those cookies are purged. + + // we test the following cases of eviction: + // 1) excess and age are satisfied, but only some of the excess are old enough + // to be purged. + Services.cookiemgr.removeAll(); + if (!set_cookies(0, 5, expiry)) { + repeat_test(); + return; + } + // Sleep a while, to make sure the first batch of cookies is older than + // the second (timer resolution varies on different platforms). + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(5, 111, expiry)) { + repeat_test(); + return; + } + + // Fake a profile change, to ensure eviction affects the database correctly. + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 5, 106)); + + // 2) excess and age are satisfied, and all of the excess are old enough + // to be purged. + Services.cookiemgr.removeAll(); + if (!set_cookies(0, 10, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(10, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 10, 101)); + + // 3) excess and age are satisfied, and more than the excess are old enough + // to be purged. + Services.cookiemgr.removeAll(); + if (!set_cookies(0, 50, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(50, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 50, 101)); + + // 4) excess but not age are satisfied. + Services.cookiemgr.removeAll(); + if (!set_cookies(0, 120, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(120, 0, 120)); + + // 5) age but not excess are satisfied. + Services.cookiemgr.removeAll(); + if (!set_cookies(0, 20, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(20, 110, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(110, 20, 110)); + + // 6) Excess and age are satisfied, but the cookie limit can be satisfied by + // purging expired cookies. + Services.cookiemgr.removeAll(); + let shortExpiry = Math.floor(Date.now() / 1000) + gShortExpiry; + if (!set_cookies(0, 20, shortExpiry)) { + repeat_test(); + return; + } + do_timeout(get_expiry_delay(), continue_test); + yield; + if (!set_cookies(20, 110, expiry)) { + repeat_test(); + return; + } + do_timeout(get_purge_delay(), continue_test); + yield; + if (!set_cookies(110, 111, expiry)) { + repeat_test(); + return; + } + + do_close_profile(test_generator); + yield; + do_load_profile(); + Assert.ok(check_remaining_cookies(111, 20, 91)); + + do_finish_generator_test(test_generator); +} + +// Set 'end - begin' total cookies, with consecutively increasing hosts numbered +// 'begin' to 'end'. +function set_cookies(begin, end, expiry) { + Assert.ok(begin != end); + + let beginTime; + for (let i = begin; i < end; ++i) { + let host = "eviction." + i + ".tests"; + Services.cookiemgr.add( + host, + "", + "test", + "eviction", + false, + false, + false, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + if (i == begin) { + beginTime = get_creationTime(i); + } + } + + let endTime = get_creationTime(end - 1); + Assert.ok(begin == end - 1 || endTime > beginTime); + if (endTime - beginTime > gPurgeAge * 1000000) { + // Setting cookies took an amount of time very close to the purge threshold. + // Retry the test with a larger threshold. + return false; + } + + return true; +} + +function get_creationTime(i) { + let host = "eviction." + i + ".tests"; + let cookies = Services.cookiemgr.getCookiesFromHost(host, {}); + Assert.ok(cookies.length); + let cookie = cookies[0]; + return cookie.creationTime; +} + +// Test that 'aNumberToExpect' cookies remain after purging is complete, and +// that the cookies that remain consist of the set expected given the number of +// of older and newer cookies -- eviction should occur by order of lastAccessed +// time, if both the limit on total cookies (maxNumber + 10%) and the purge age +// + 10% are exceeded. +function check_remaining_cookies(aNumberTotal, aNumberOld, aNumberToExpect) { + let i = 0; + for (let cookie of Services.cookiemgr.cookies) { + ++i; + + if (aNumberTotal != aNumberToExpect) { + // make sure the cookie is one of the batch we expect was purged. + var hostNumber = Number(cookie.rawHost.split(".")[1]); + if (hostNumber < aNumberOld - aNumberToExpect) { + break; + } + } + } + + return i == aNumberToExpect; +} diff --git a/netwerk/test/unit/test_extract_charset_from_content_type.js b/netwerk/test/unit/test_extract_charset_from_content_type.js new file mode 100644 index 0000000000..1d12b6bc2b --- /dev/null +++ b/netwerk/test/unit/test_extract_charset_from_content_type.js @@ -0,0 +1,245 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var charset = {}; +var charsetStart = {}; +var charsetEnd = {}; +var hadCharset; + +function reset() { + delete charset.value; + delete charsetStart.value; + delete charsetEnd.value; + hadCharset = undefined; +} + +function check(aHadCharset, aCharset, aCharsetStart, aCharsetEnd) { + Assert.equal(aHadCharset, hadCharset); + Assert.equal(aCharset, charset.value); + Assert.equal(aCharsetStart, charsetStart.value); + Assert.equal(aCharsetEnd, charsetEnd.value); +} + +function run_test() { + var netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil); + hadCharset = netutil.extractCharsetFromContentType( + "text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "TEXT/HTML", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, text/plain", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 21, 21); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, ", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, */*", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html, foo", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 9, 9); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 29); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 34); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1 ", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 36); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html ; charset=ISO-8859-1 ; ", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 11, 35); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1"', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset='ISO-8859-1'", + charset, + charsetStart, + charsetEnd + ); + check(true, "'ISO-8859-1'", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1", text/html', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 31); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/html; charset="ISO-8859-1", text/html; charset=UTF8', + charset, + charsetStart, + charsetEnd + ); + check(true, "UTF8", 42, 56); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1, TEXT/HTML", + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 9, 29); + + hadCharset = netutil.extractCharsetFromContentType( + "text/html; charset=ISO-8859-1, TEXT/plain", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 41, 41); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; charset="ISO-8859-1", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 21, 43); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; charset="ISO-8859-1"; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 43, 65); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param=charset=UTF8; charset="ISO-8859-1"; param2=charset=UTF16, text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(true, "ISO-8859-1", 41, 63); + + hadCharset = netutil.extractCharsetFromContentType( + "text/plain; param= , text/html", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 30, 30); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", text/html"', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", \\" , text/html"', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain; param=", \\" , text/html , "', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 10, 10); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain param=", \\" , text/html , "', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 38, 38); + + hadCharset = netutil.extractCharsetFromContentType( + "text/plain charset=UTF8", + charset, + charsetStart, + charsetEnd + ); + check(false, "", 23, 23); + + hadCharset = netutil.extractCharsetFromContentType( + 'text/plain, TEXT/HTML; param="charset=UTF8"; ; param2="charset=UTF16", text/html, TEXT/HTML', + charset, + charsetStart, + charsetEnd + ); + check(false, "", 21, 21); +} diff --git a/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js b/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js new file mode 100644 index 0000000000..d5ca20badf --- /dev/null +++ b/netwerk/test/unit/test_fallback_no-cache-entry_canceled.js @@ -0,0 +1,131 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + // In this test the randomURI doesn't exists + var chan = make_channel(randomURI); + chan.loadFlags = + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_FROM_CACHE | + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE; + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_no-cache-entry_passing.js b/netwerk/test/unit/test_fallback_no-cache-entry_passing.js new file mode 100644 index 0000000000..9bbadb4631 --- /dev/null +++ b/netwerk/test/unit/test_fallback_no-cache-entry_passing.js @@ -0,0 +1,130 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + // In this test the randomURI doesn't exists + var chan = make_channel(randomURI); + chan.loadFlags = + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIRequest.LOAD_FROM_CACHE | + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE; + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js b/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js new file mode 100644 index 0000000000..d04797cacf --- /dev/null +++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_canceled.js @@ -0,0 +1,133 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +const responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// redirect handler returns redirect +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://example.com/", false); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + var chan = make_channel(randomURI); + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js b/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js new file mode 100644 index 0000000000..dc1702da3f --- /dev/null +++ b/netwerk/test/unit/test_fallback_redirect-to-different-origin_passing.js @@ -0,0 +1,132 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// redirect handler returns redirect +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://example.com/", false); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.registerPathHandler(randomPath, redirectHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + var chan = make_channel(randomURI); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_request-error_canceled.js b/netwerk/test/unit/test_fallback_request-error_canceled.js new file mode 100644 index 0000000000..8f1eb8b597 --- /dev/null +++ b/netwerk/test/unit/test_fallback_request-error_canceled.js @@ -0,0 +1,140 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// redirect handler returns redirect +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://example.com/", false); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + do_test_finished(); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + + // doing this to eval the lazy getter, otherwise the make_channel call would hang + randomURI; + httpServer.stop(function() { + // Now shut the server down to have an error in onStartRequest + var chan = make_channel(randomURI); + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen( + new ChannelListener(finish_test, null, CL_EXPECT_FAILURE) + ); + }); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_request-error_passing.js b/netwerk/test/unit/test_fallback_request-error_passing.js new file mode 100644 index 0000000000..89a672ee4c --- /dev/null +++ b/netwerk/test/unit/test_fallback_request-error_passing.js @@ -0,0 +1,136 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/redirect/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; +var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /redirect path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nredirect /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// redirect handler returns redirect +function redirectHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 301, "Moved"); + response.setHeader("Location", "http://example.com/", false); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + do_test_finished(); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + randomURI; // doing this so the lazy value gets computed + httpServer.stop(function() { + // Now shut the server down to have an error in onstartrequest + var chan = make_channel(randomURI); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test)); + }); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + systemPrincipal, + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_response-error_canceled.js b/netwerk/test/unit/test_fallback_response-error_canceled.js new file mode 100644 index 0000000000..5bd8a2d222 --- /dev/null +++ b/netwerk/test/unit/test_fallback_response-error_canceled.js @@ -0,0 +1,133 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/error/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; +var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /error path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nerror /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// error handler returns error +function errorHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 404, "Bad request"); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, ""); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.registerPathHandler(randomPath, errorHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + var chan = make_channel(randomURI); + chan.notificationCallbacks = new ChannelEventSink(ES_ABORT_REDIRECT); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + systemPrincipal, + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_fallback_response-error_passing.js b/netwerk/test/unit/test_fallback_response-error_passing.js new file mode 100644 index 0000000000..c9715a916a --- /dev/null +++ b/netwerk/test/unit/test_fallback_response-error_passing.js @@ -0,0 +1,132 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpServer = null; +// Need to randomize, because apparently no one clears our cache +var randomPath = "/error/" + Math.random(); + +XPCOMUtils.defineLazyGetter(this, "randomURI", function() { + return "http://localhost:" + httpServer.identity.primaryPort + randomPath; +}); + +var cacheUpdateObserver = null; +var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +function make_uri(url) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return ios.newURI(url); +} + +var responseBody = "Content body"; + +// start the test with loading this master entry referencing the manifest +function masterEntryHandler(metadata, response) { + var masterEntryContent = ""; + response.setHeader("Content-Type", "text/html"); + response.bodyOutputStream.write( + masterEntryContent, + masterEntryContent.length + ); +} + +// manifest defines fallback namespace from any /error path to /content +function manifestHandler(metadata, response) { + var manifestContent = "CACHE MANIFEST\nFALLBACK:\nerror /content\n"; + response.setHeader("Content-Type", "text/cache-manifest"); + response.bodyOutputStream.write(manifestContent, manifestContent.length); +} + +// content handler correctly returns some plain text data +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +// error handler returns error +function errorHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 404, "Bad request"); +} + +// finally check we got fallback content +function finish_test(request, buffer) { + Assert.equal(buffer, responseBody); + httpServer.stop(do_test_finished); +} + +function run_test() { + httpServer = new HttpServer(); + httpServer.registerPathHandler("/masterEntry", masterEntryHandler); + httpServer.registerPathHandler("/manifest", manifestHandler); + httpServer.registerPathHandler("/content", contentHandler); + httpServer.registerPathHandler(randomPath, errorHandler); + httpServer.start(-1); + + var pm = Cc["@mozilla.org/permissionmanager;1"].getService( + Ci.nsIPermissionManager + ); + var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService( + Ci.nsIScriptSecurityManager + ); + var uri = make_uri("http://localhost:" + httpServer.identity.primaryPort); + var principal = ssm.createContentPrincipal(uri, {}); + + if (pm.testPermissionFromPrincipal(principal, "offline-app") != 0) { + dump( + "Previous test failed to clear offline-app permission! Expect failures.\n" + ); + } + pm.addFromPrincipal( + principal, + "offline-app", + Ci.nsIPermissionManager.ALLOW_ACTION + ); + + var ps = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + dump(ps.getBoolPref("browser.cache.offline.enable")); + ps.setBoolPref("browser.cache.offline.enable", true); + ps.setBoolPref("browser.cache.offline.storage.enable", true); + ps.setComplexValue( + "browser.cache.offline.parent_directory", + Ci.nsIFile, + do_get_profile() + ); + + cacheUpdateObserver = { + observe() { + dump("got offline-cache-update-completed\n"); + // offline cache update completed. + var chan = make_channel(randomURI); + var chanac = chan.QueryInterface(Ci.nsIApplicationCacheChannel); + chanac.chooseApplicationCache = true; + chan.asyncOpen(new ChannelListener(finish_test)); + }, + }; + + var os = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService + ); + os.addObserver(cacheUpdateObserver, "offline-cache-update-completed"); + + var us = Cc["@mozilla.org/offlinecacheupdate-service;1"].getService( + Ci.nsIOfflineCacheUpdateService + ); + us.scheduleUpdate( + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/manifest" + ), + make_uri( + "http://localhost:" + httpServer.identity.primaryPort + "/masterEntry" + ), + systemPrincipal, + null + ); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_file_protocol.js b/netwerk/test/unit/test_file_protocol.js new file mode 100644 index 0000000000..9aa4cf4183 --- /dev/null +++ b/netwerk/test/unit/test_file_protocol.js @@ -0,0 +1,280 @@ +/* run some tests on the file:// protocol handler */ + +"use strict"; + +const PR_RDONLY = 0x1; // see prio.h + +const special_type = "application/x-our-special-type"; + +[ + test_read_file, + test_read_dir_1, + test_read_dir_2, + test_upload_file, + test_load_shelllink, + do_test_finished, +].forEach(f => add_test(f)); + +function getFile(key) { + var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( + Ci.nsIProperties + ); + return dirSvc.get(key, Ci.nsIFile); +} + +function new_file_input_stream(file, buffered) { + var stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + stream.init(file, PR_RDONLY, 0, 0); + if (!buffered) { + return stream; + } + + var buffer = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffer.init(stream, 4096); + return buffer; +} + +function new_file_channel(file) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + return NetUtil.newChannel({ + uri: ios.newFileURI(file), + loadUsingSystemPrincipal: true, + }); +} + +/* + * stream listener + * this listener has some additional file-specific tests, so we can't just use + * ChannelListener here. + */ +function FileStreamListener(closure) { + this._closure = closure; +} +FileStreamListener.prototype = { + _closure: null, + _buffer: "", + _got_onstartrequest: false, + _got_onstoprequest: false, + _contentLen: -1, + + _isDir(request) { + request.QueryInterface(Ci.nsIFileChannel); + return request.file.isDirectory(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + if (this._got_onstartrequest) { + do_throw("Got second onStartRequest event!"); + } + this._got_onstartrequest = true; + + if (!this._isDir(request)) { + request.QueryInterface(Ci.nsIChannel); + this._contentLen = request.contentLength; + if (this._contentLen == -1) { + do_throw("Content length is unknown in onStartRequest!"); + } + } + }, + + onDataAvailable(request, stream, offset, count) { + if (!this._got_onstartrequest) { + do_throw("onDataAvailable without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("onDataAvailable after onStopRequest event!"); + } + if (!request.isPending()) { + do_throw("request reports itself as not pending from onStartRequest!"); + } + + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + + onStopRequest(request, status) { + if (!this._got_onstartrequest) { + do_throw("onStopRequest without onStartRequest event!"); + } + if (this._got_onstoprequest) { + do_throw("Got second onStopRequest event!"); + } + this._got_onstoprequest = true; + if (!Components.isSuccessCode(status)) { + do_throw("Failed to load file: " + status.toString(16)); + } + if (status != request.status) { + do_throw("request.status does not match status arg to onStopRequest!"); + } + if (request.isPending()) { + do_throw("request reports itself as pending from onStopRequest!"); + } + if (this._contentLen != -1 && this._buffer.length != this._contentLen) { + do_throw("did not read nsIChannel.contentLength number of bytes!"); + } + + this._closure(this._buffer); + }, +}; + +function test_read_file() { + dump("*** test_read_file\n"); + + var file = do_get_file("../unit/data/test_readline6.txt"); + var chan = new_file_channel(file); + + function on_read_complete(data) { + dump("*** test_read_file.on_read_complete\n"); + + // bug 326693 + if (chan.contentType != special_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + special_type + + ">" + ); + } + + /* read completed successfully. now read data directly from file, + and compare the result. */ + var stream = new_file_input_stream(file, false); + var result = read_stream(stream, stream.available()); + if (result != data) { + do_throw("Stream contents do not match with direct read!"); + } + run_next_test(); + } + + chan.contentType = special_type; + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function do_test_read_dir(set_type, expected_type) { + dump("*** test_read_dir(" + set_type + ", " + expected_type + ")\n"); + + var file = do_get_tempdir(); + var chan = new_file_channel(file); + + function on_read_complete(data) { + dump( + "*** test_read_dir.on_read_complete(" + + set_type + + ", " + + expected_type + + ")\n" + ); + + // bug 326693 + if (chan.contentType != expected_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + expected_type + + ">" + ); + } + + run_next_test(); + } + + if (set_type) { + chan.contentType = expected_type; + } + chan.asyncOpen(new FileStreamListener(on_read_complete)); +} + +function test_read_dir_1() { + return do_test_read_dir(false, "application/http-index-format"); +} + +function test_read_dir_2() { + return do_test_read_dir(true, special_type); +} + +function test_upload_file() { + dump("*** test_upload_file\n"); + + var file = do_get_file("../unit/data/test_readline6.txt"); // file to upload + var dest = do_get_tempdir(); // file upload destination + dest.append("junk.dat"); + dest.createUnique(dest.NORMAL_FILE_TYPE, 0o600); + + var uploadstream = new_file_input_stream(file, true); + + var chan = new_file_channel(dest); + chan.QueryInterface(Ci.nsIUploadChannel); + chan.setUploadStream(uploadstream, "", file.fileSize); + + function on_upload_complete(data) { + dump("*** test_upload_file.on_upload_complete\n"); + + // bug 326693 + if (chan.contentType != special_type) { + do_throw( + "Type mismatch! Is <" + + chan.contentType + + ">, should be <" + + special_type + + ">" + ); + } + + /* upload of file completed successfully. */ + if (data.length != 0) { + do_throw("Upload resulted in data!"); + } + + var oldstream = new_file_input_stream(file, false); + var newstream = new_file_input_stream(dest, false); + var olddata = read_stream(oldstream, oldstream.available()); + var newdata = read_stream(newstream, newstream.available()); + if (olddata != newdata) { + do_throw("Stream contents do not match after file copy!"); + } + oldstream.close(); + newstream.close(); + + /* cleanup... also ensures that the destination file is not in + use when OnStopRequest is called. */ + try { + dest.remove(false); + } catch (e) { + dump(e + "\n"); + do_throw("Unable to remove uploaded file!\n"); + } + + run_next_test(); + } + + chan.contentType = special_type; + chan.asyncOpen(new FileStreamListener(on_upload_complete)); +} + +function test_load_shelllink() { + // lnk files should not resolve to their targets + dump("*** test_load_shelllink\n"); + let file = do_get_file("data/system_root.lnk", false); + var chan = new_file_channel(file); + + // The original URI path should be the same as the URI path + Assert.equal(chan.URI.pathQueryRef, chan.originalURI.pathQueryRef); + + // The original URI path should be the same as the lnk file path + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + Assert.equal( + chan.originalURI.pathQueryRef, + ios.newFileURI(file).pathQueryRef + ); + run_next_test(); +} diff --git a/netwerk/test/unit/test_filestreams.js b/netwerk/test/unit/test_filestreams.js new file mode 100644 index 0000000000..d8beec0b8a --- /dev/null +++ b/netwerk/test/unit/test_filestreams.js @@ -0,0 +1,306 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// We need the profile directory so the test harness will clean up our test +// files. +do_get_profile(); + +const OUTPUT_STREAM_CONTRACT_ID = "@mozilla.org/network/file-output-stream;1"; +const SAFE_OUTPUT_STREAM_CONTRACT_ID = + "@mozilla.org/network/safe-file-output-stream;1"; + +//////////////////////////////////////////////////////////////////////////////// +//// Helper Methods + +/** + * Generates a leafName for a file that does not exist, but does *not* + * create the file. Similar to createUnique except for the fact that createUnique + * does create the file. + * + * @param aFile + * The file to modify in order for it to have a unique leafname. + */ +function ensure_unique(aFile) { + ensure_unique.fileIndex = ensure_unique.fileIndex || 0; + + var leafName = aFile.leafName; + while (aFile.clone().exists()) { + aFile.leafName = leafName + "_" + ensure_unique.fileIndex++; + } +} + +/** + * Tests for files being accessed at the right time. Streams that use + * DEFER_OPEN should only open or create the file when an operation is + * done, and not during Init(). + * + * Note that for writing, we check for actual writing in test_NetUtil (async) + * and in sync_operations in this file (sync), whereas in this function we + * just check that the file is *not* created during init. + * + * @param aContractId + * The contract ID to use for the output stream + * @param aDeferOpen + * Whether to check with DEFER_OPEN or not + * @param aTrickDeferredOpen + * Whether we try to 'trick' deferred opens by changing the file object before + * the actual open. The stream should have a clone, so changes to the file + * object after Init and before Open should not affect it. + */ +function check_access(aContractId, aDeferOpen, aTrickDeferredOpen) { + const LEAF_NAME = "filestreams-test-file.tmp"; + const TRICKY_LEAF_NAME = "BetYouDidNotExpectThat.tmp"; + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + + // Writing + + ensure_unique(file); + let ostream = Cc[aContractId].createInstance(Ci.nsIFileOutputStream); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + Assert.equal(aDeferOpen, !file.clone().exists()); // If defer, should not exist and vice versa + if (aDeferOpen) { + // File should appear when we do write to it. + if (aTrickDeferredOpen) { + // See |@param aDeferOpen| in the JavaDoc comment for this function + file.leafName = TRICKY_LEAF_NAME; + } + ostream.write("data", 4); + if (aTrickDeferredOpen) { + file.leafName = LEAF_NAME; + } + // We did a write, so the file should now exist + Assert.ok(file.clone().exists()); + } + ostream.close(); + + // Reading + + ensure_unique(file); + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var initOk, getOk; + try { + istream.init( + file, + -1, + 0, + aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0 + ); + initOk = true; + } catch (e) { + initOk = false; + } + try { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, 0); + getOk = true; + } catch (e) { + getOk = false; + } + + // If the open is deferred, then Init should succeed even though the file we + // intend to read does not exist, and then trying to read from it should + // fail. The other case is where the open is not deferred, and there we should + // get an error when we Init (and also when we try to read). + Assert.ok( + (aDeferOpen && initOk && !getOk) || (!aDeferOpen && !initOk && !getOk) + ); + istream.close(); +} + +/** + * We test async operations in test_NetUtil.js, and here test for simple sync + * operations on input streams. + * + * @param aDeferOpen + * Whether to use DEFER_OPEN in the streams. + */ +function sync_operations(aDeferOpen) { + const TEST_DATA = "this is a test string"; + const LEAF_NAME = "filestreams-test-file.tmp"; + + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let ostream = Cc[OUTPUT_STREAM_CONTRACT_ID].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init( + file, + -1, + -1, + aDeferOpen ? Ci.nsIFileOutputStream.DEFER_OPEN : 0 + ); + + ostream.write(TEST_DATA, TEST_DATA.length); + ostream.close(); + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, 0, aDeferOpen ? Ci.nsIFileInputStream.DEFER_OPEN : 0); + + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(fstream, "UTF-8", 0, 0); + + let string = {}; + cstream.readString(-1, string); + cstream.close(); + fstream.close(); + + Assert.equal(string.value, TEST_DATA); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +function test_access() { + check_access(OUTPUT_STREAM_CONTRACT_ID, false, false); +} + +function test_access_trick() { + check_access(OUTPUT_STREAM_CONTRACT_ID, false, true); +} + +function test_access_defer() { + check_access(OUTPUT_STREAM_CONTRACT_ID, true, false); +} + +function test_access_defer_trick() { + check_access(OUTPUT_STREAM_CONTRACT_ID, true, true); +} + +function test_access_safe() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, false); +} + +function test_access_safe_trick() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, false, true); +} + +function test_access_safe_defer() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, false); +} + +function test_access_safe_defer_trick() { + check_access(SAFE_OUTPUT_STREAM_CONTRACT_ID, true, true); +} + +function test_sync_operations() { + sync_operations(); +} + +function test_sync_operations_deferred() { + sync_operations(true); +} + +function do_test_zero_size_buffered(disableBuffering) { + const LEAF_NAME = "filestreams-test-file.tmp"; + const BUFFERSIZE = 4096; + + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append(LEAF_NAME); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init( + file, + -1, + 0, + Ci.nsIFileInputStream.CLOSE_ON_EOF | Ci.nsIFileInputStream.REOPEN_ON_REWIND + ); + + var buffered = Cc[ + "@mozilla.org/network/buffered-input-stream;1" + ].createInstance(Ci.nsIBufferedInputStream); + buffered.init(fstream, BUFFERSIZE); + + if (disableBuffering) { + buffered.QueryInterface(Ci.nsIStreamBufferAccess).disableBuffering(); + } + + // Scriptable input streams clamp read sizes to the return value of + // available(), so don't quite do what we want here. + let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + cstream.init(buffered, "UTF-8", 0, 0); + + Assert.equal(buffered.available(), 0); + + // Now try reading from this stream + let string = {}; + Assert.equal(cstream.readString(BUFFERSIZE, string), 0); + Assert.equal(string.value, ""); + + // Now check that available() throws + var exceptionThrown = false; + try { + Assert.equal(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + Assert.ok(exceptionThrown); + + // OK, now seek back to start + buffered.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + + // Now check that available() does not throw + exceptionThrown = false; + try { + Assert.equal(buffered.available(), 0); + } catch (e) { + exceptionThrown = true; + } + Assert.ok(!exceptionThrown); +} + +function test_zero_size_buffered() { + do_test_zero_size_buffered(false); + do_test_zero_size_buffered(true); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Runner + +var tests = [ + test_access, + test_access_trick, + test_access_defer, + test_access_defer_trick, + test_access_safe, + test_access_safe_trick, + test_access_safe_defer, + test_access_safe_defer_trick, + test_sync_operations, + test_sync_operations_deferred, + test_zero_size_buffered, +]; + +function run_test() { + tests.forEach(function(test) { + test(); + }); +} diff --git a/netwerk/test/unit/test_freshconnection.js b/netwerk/test/unit/test_freshconnection.js new file mode 100644 index 0000000000..5d0f5bc5b7 --- /dev/null +++ b/netwerk/test/unit/test_freshconnection.js @@ -0,0 +1,30 @@ +// This is essentially a debug mode crashtest to make sure everything +// involved in a reload runs on the right thread. It relies on the +// assertions in necko. + +"use strict"; + +var listener = { + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + do_test_finished(); + }, +}; + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://localhost:4444", + loadUsingSystemPrincipal: true, + }); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + chan.QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(listener); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_getHost.js b/netwerk/test/unit/test_getHost.js new file mode 100644 index 0000000000..90db800575 --- /dev/null +++ b/netwerk/test/unit/test_getHost.js @@ -0,0 +1,63 @@ +// Test getLocalHost/getLocalPort and getRemoteHost/getRemotePort. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = new HttpServer(); +httpserver.start(-1); +const PORT = httpserver.identity.primaryPort; + +var gotOnStartRequest = false; + +function CheckGetHostListener() {} + +CheckGetHostListener.prototype = { + onStartRequest(request) { + dump("*** listener onStartRequest\n"); + + gotOnStartRequest = true; + + request.QueryInterface(Ci.nsIHttpChannelInternal); + try { + Assert.equal(request.localAddress, "127.0.0.1"); + Assert.equal(request.localPort > 0, true); + Assert.notEqual(request.localPort, PORT); + Assert.equal(request.remoteAddress, "127.0.0.1"); + Assert.equal(request.remotePort, PORT); + } catch (e) { + Assert.ok(0, "Get local/remote host/port throws an error!"); + } + }, + + onStopRequest(request, statusCode) { + dump("*** listener onStopRequest\n"); + + Assert.equal(gotOnStartRequest, true); + httpserver.stop(do_test_finished); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIRequestObserver"]), +}; + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + var responseBody = "blah blah"; + response.bodyOutputStream.write(responseBody, responseBody.length); +} + +function run_test() { + httpserver.registerPathHandler("/testdir", test_handler); + + var channel = make_channel("http://localhost:" + PORT + "/testdir"); + channel.asyncOpen(new CheckGetHostListener()); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_gre_resources.js b/netwerk/test/unit/test_gre_resources.js new file mode 100644 index 0000000000..098dae660d --- /dev/null +++ b/netwerk/test/unit/test_gre_resources.js @@ -0,0 +1,32 @@ +// test that things that are expected to be in gre-resources are still there + +"use strict"; + +var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + +function wrapInputStream(input) { + var nsIScriptableInputStream = Ci.nsIScriptableInputStream; + var factory = Cc["@mozilla.org/scriptableinputstream;1"]; + var wrapper = factory.createInstance(nsIScriptableInputStream); + wrapper.init(input); + return wrapper; +} + +function check_file(file) { + var channel = NetUtil.newChannel({ + uri: "resource://gre-resources/" + file, + loadUsingSystemPrincipal: true, + }); + try { + let instr = wrapInputStream(channel.open()); + Assert.ok(instr.read(1024).length > 0); + } catch (e) { + do_throw("Failed to read " + file + " from gre-resources:" + e); + } +} + +function run_test() { + for (let file of ["ua.css"]) { + check_file(file); + } +} diff --git a/netwerk/test/unit/test_gzipped_206.js b/netwerk/test/unit/test_gzipped_206.js new file mode 100644 index 0000000000..a60bef3d26 --- /dev/null +++ b/netwerk/test/unit/test_gzipped_206.js @@ -0,0 +1,167 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserver = null; + +// testString = "This is a slightly longer test\n"; +const responseBody = [ + 0x1f, + 0x8b, + 0x08, + 0x08, + 0xef, + 0x70, + 0xe6, + 0x4c, + 0x00, + 0x03, + 0x74, + 0x65, + 0x78, + 0x74, + 0x66, + 0x69, + 0x6c, + 0x65, + 0x2e, + 0x74, + 0x78, + 0x74, + 0x00, + 0x0b, + 0xc9, + 0xc8, + 0x2c, + 0x56, + 0x00, + 0xa2, + 0x44, + 0x85, + 0xe2, + 0x9c, + 0xcc, + 0xf4, + 0x8c, + 0x92, + 0x9c, + 0x4a, + 0x85, + 0x9c, + 0xfc, + 0xbc, + 0xf4, + 0xd4, + 0x22, + 0x85, + 0x92, + 0xd4, + 0xe2, + 0x12, + 0x2e, + 0x2e, + 0x00, + 0x00, + 0xe5, + 0xe6, + 0xf0, + 0x20, + 0x00, + 0x00, + 0x00, +]; + +function make_channel(url, callback, ctx) { + return NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); +} + +var doRangeResponse = false; + +function cachedHandler(metadata, response) { + response.setHeader("Content-Type", "application/x-gzip", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=3600000"); // avoid validation + + var body = responseBody; + + if (doRangeResponse) { + Assert.ok(metadata.hasHeader("Range")); + var matches = metadata + .getHeader("Range") + .match(/^\s*bytes=(\d+)?-(\d+)?\s*$/); + var from = matches[1] === undefined ? 0 : matches[1]; + var to = matches[2] === undefined ? responseBody.length - 1 : matches[2]; + if (from >= responseBody.length) { + response.setStatusLine(metadata.httpVersion, 416, "Start pos too high"); + response.setHeader("Content-Range", "*/" + responseBody.length, false); + return; + } + body = body.slice(from, to + 1); + response.setHeader("Content-Length", "" + (to + 1 - from)); + // always respond to successful range requests with 206 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader( + "Content-Range", + from + "-" + to + "/" + responseBody.length, + false + ); + } else { + // This response will get cut off prematurely + response.setHeader("Content-Length", "" + responseBody.length); + response.setHeader("Accept-Ranges", "bytes"); + body = body.slice(0, 17); // slice off a piece to send first + doRangeResponse = true; + } + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + bos.setOutputStream(response.bodyOutputStream); + + response.processAsync(); + bos.writeByteArray(body); + response.finish(); +} + +function continue_test(request, data) { + Assert.equal(17, data.length); + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_GZIP)); +} + +var enforcePref; + +function finish_test(request, data, ctx) { + Assert.equal(request.status, 0); + Assert.equal(data.length, responseBody.length); + for (var i = 0; i < data.length; ++i) { + Assert.equal(data.charCodeAt(i), responseBody[i]); + } + Services.prefs.setBoolPref("network.http.enforce-framing.http1", enforcePref); + httpserver.stop(do_test_finished); +} + +function run_test() { + enforcePref = Services.prefs.getBoolPref( + "network.http.enforce-framing.http1" + ); + Services.prefs.setBoolPref("network.http.enforce-framing.http1", false); + + httpserver = new HttpServer(); + httpserver.registerPathHandler("/cached/test.gz", cachedHandler); + httpserver.start(-1); + + // wipe out cached content + evict_cache_entries(); + + var chan = make_channel( + "http://localhost:" + httpserver.identity.primaryPort + "/cached/test.gz" + ); + chan.asyncOpen( + new ChannelListener(continue_test, null, CL_EXPECT_GZIP | CL_IGNORE_CL) + ); + do_test_pending(); +} diff --git a/netwerk/test/unit/test_head.js b/netwerk/test/unit/test_head.js new file mode 100644 index 0000000000..c25b3e3dbc --- /dev/null +++ b/netwerk/test/unit/test_head.js @@ -0,0 +1,169 @@ +// +// HTTP headers test +// + +// Note: sets Cc and Ci variables + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var testpath = "/simple"; +var httpbody = "0123456789"; +var channel; +var ios; + +var dbg = 0; +if (dbg) { + print("============== START =========="); +} + +function run_test() { + setup_test(); + do_test_pending(); +} + +function setup_test() { + if (dbg) { + print("============== setup_test: in"); + } + + httpserver.registerPathHandler(testpath, serverHandler); + httpserver.start(-1); + + channel = setupChannel(testpath); + + channel.setRequestHeader("ReplaceMe", "initial value", true); + var setOK = channel.getRequestHeader("ReplaceMe"); + Assert.equal(setOK, "initial value"); + channel.setRequestHeader("ReplaceMe", "replaced", false); + setOK = channel.getRequestHeader("ReplaceMe"); + Assert.equal(setOK, "replaced"); + + channel.setRequestHeader("MergeMe", "foo1", true); + channel.setRequestHeader("MergeMe", "foo2", true); + channel.setRequestHeader("MergeMe", "foo3", true); + setOK = channel.getRequestHeader("MergeMe"); + Assert.equal(setOK, "foo1, foo2, foo3"); + + channel.setEmptyRequestHeader("Empty"); + setOK = channel.getRequestHeader("Empty"); + Assert.equal(setOK, ""); + + channel.setRequestHeader("ReplaceWithEmpty", "initial value", true); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + Assert.equal(setOK, "initial value"); + channel.setEmptyRequestHeader("ReplaceWithEmpty"); + setOK = channel.getRequestHeader("ReplaceWithEmpty"); + Assert.equal(setOK, ""); + + channel.setEmptyRequestHeader("MergeWithEmpty"); + setOK = channel.getRequestHeader("MergeWithEmpty"); + Assert.equal(setOK, ""); + channel.setRequestHeader("MergeWithEmpty", "foo", true); + setOK = channel.getRequestHeader("MergeWithEmpty"); + Assert.equal(setOK, "foo"); + + var uri = NetUtil.newURI("http://foo1.invalid:80"); + channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + setOK = channel.getRequestHeader("Referer"); + Assert.equal(setOK, "http://foo1.invalid/"); + + uri = NetUtil.newURI("http://foo2.invalid:90/bar"); + channel.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + setOK = channel.getRequestHeader("Referer"); + Assert.equal(setOK, "http://foo2.invalid:90/bar"); + + // ChannelListener defined in head_channels.js + channel.asyncOpen(new ChannelListener(checkRequestResponse, channel)); + + if (dbg) { + print("============== setup_test: out"); + } +} + +function setupChannel(path) { + var chan = NetUtil.newChannel({ + uri: URL + path, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + chan.requestMethod = "GET"; + return chan; +} + +function serverHandler(metadata, response) { + if (dbg) { + print("============== serverHandler: in"); + } + + var setOK = metadata.getHeader("ReplaceMe"); + Assert.equal(setOK, "replaced"); + setOK = metadata.getHeader("MergeMe"); + Assert.equal(setOK, "foo1, foo2, foo3"); + setOK = metadata.getHeader("Empty"); + Assert.equal(setOK, ""); + setOK = metadata.getHeader("ReplaceWithEmpty"); + Assert.equal(setOK, ""); + setOK = metadata.getHeader("MergeWithEmpty"); + Assert.equal(setOK, "foo"); + setOK = metadata.getHeader("Referer"); + Assert.equal(setOK, "http://foo2.invalid:90/bar"); + + response.setHeader("Content-Type", "text/plain", false); + response.setStatusLine("1.1", 200, "OK"); + + // note: httpd.js' "Response" class uses ',' (no space) for merge. + response.setHeader("httpdMerge", "bar1", false); + response.setHeader("httpdMerge", "bar2", true); + response.setHeader("httpdMerge", "bar3", true); + // Some special headers like Proxy-Authenticate merge with \n + response.setHeader("Proxy-Authenticate", "line 1", true); + response.setHeader("Proxy-Authenticate", "line 2", true); + response.setHeader("Proxy-Authenticate", "line 3", true); + + response.bodyOutputStream.write(httpbody, httpbody.length); + + if (dbg) { + print("============== serverHandler: out"); + } +} + +function checkRequestResponse(request, data, context) { + if (dbg) { + print("============== checkRequestResponse: in"); + } + + Assert.equal(channel.responseStatus, 200); + Assert.equal(channel.responseStatusText, "OK"); + Assert.ok(channel.requestSucceeded); + + var response = channel.getResponseHeader("httpdMerge"); + Assert.equal(response, "bar1,bar2,bar3"); + channel.setResponseHeader("httpdMerge", "bar", true); + Assert.equal(channel.getResponseHeader("httpdMerge"), "bar1,bar2,bar3, bar"); + + response = channel.getResponseHeader("Proxy-Authenticate"); + Assert.equal(response, "line 1\nline 2\nline 3"); + + channel.contentCharset = "UTF-8"; + Assert.equal(channel.contentCharset, "UTF-8"); + Assert.equal(channel.contentType, "text/plain"); + Assert.equal(channel.contentLength, httpbody.length); + Assert.equal(data, httpbody); + + httpserver.stop(do_test_finished); + if (dbg) { + print("============== checkRequestResponse: out"); + } +} diff --git a/netwerk/test/unit/test_head_request_no_response_body.js b/netwerk/test/unit/test_head_request_no_response_body.js new file mode 100644 index 0000000000..df319cbaee --- /dev/null +++ b/netwerk/test/unit/test_head_request_no_response_body.js @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + +Test that a response to HEAD method should not have a body. +1. Create a GET request and write the response into cache. +2. Create the second GET request with the same URI and see if the response is + from cache. +3. Create a HEAD request and test if we got a response with an empty body. + +*/ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const responseContent = "response body"; + +function test_handler(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-control", "max-age=9999", false); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + if (metadata.method != "HEAD") { + response.bodyOutputStream.write(responseContent, responseContent.length); + } +} + +function make_channel(url, method) { + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + channel.requestMethod = method; + return channel; +} + +async function get_response(channel, fromCache) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener((request, buffer, ctx, isFromCache) => { + ok(fromCache == isFromCache, `got response from cache = ${fromCache}`); + resolve(buffer); + }) + ); + }); +} + +async function stop_server(httpserver) { + return new Promise(resolve => { + httpserver.stop(resolve); + }); +} + +add_task(async function() { + let httpserver = new HttpServer(); + httpserver.registerPathHandler("/testdir", test_handler); + httpserver.start(-1); + const PORT = httpserver.identity.primaryPort; + const URI = `http://localhost:${PORT}/testdir`; + + let response; + + response = await get_response(make_channel(URI, "GET"), false); + ok(response === responseContent, "got response body"); + + response = await get_response(make_channel(URI, "GET"), true); + ok(response === responseContent, "got response body from cache"); + + response = await get_response(make_channel(URI, "HEAD"), false); + ok(response === "", "should have empty body"); + + await stop_server(httpserver); +}); diff --git a/netwerk/test/unit/test_header_Accept-Language.js b/netwerk/test/unit/test_header_Accept-Language.js new file mode 100644 index 0000000000..f3b3571992 --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language.js @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// +// HTTP Accept-Language header test +// + +"use strict"; + +var testpath = "/bug672448"; + +function run_test() { + let intlPrefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService) + .getBranch("intl."); + + // Save old value of preference for later. + let oldPref = intlPrefs.getCharPref("accept_languages"); + + // Test different numbers of languages, to test different fractions. + let acceptLangTests = [ + "qaa", // 1 + "qaa,qab", // 2 + "qaa,qab,qac,qad", // 4 + "qaa,qab,qac,qad,qae,qaf,qag,qah", // 8 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj", // 10 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak", // 11 + "qaa,qab,qac,qad,qae,qaf,qag,qah,qai,qaj,qak,qal,qam,qan,qao,qap,qaq,qar,qas,qat,qau", // 21 + oldPref, // Restore old value of preference (and test it). + ]; + + let acceptLangTestsNum = acceptLangTests.length; + + for (let i = 0; i < acceptLangTestsNum; i++) { + // Set preference to test value. + intlPrefs.setCharPref("accept_languages", acceptLangTests[i]); + + // Test value. + test_accepted_languages(); + } +} + +function test_accepted_languages() { + let channel = setupChannel(testpath); + + let AcceptLanguage = channel.getRequestHeader("Accept-Language"); + + let acceptedLanguages = AcceptLanguage.split(","); + + let acceptedLanguagesLength = acceptedLanguages.length; + + for (let i = 0; i < acceptedLanguagesLength; i++) { + let qualityValue; + + try { + // The q-value must conform to the definition in HTTP/1.1 Section 3.9. + [, , qualityValue] = acceptedLanguages[i] + .trim() + .match(/^([a-z0-9_-]*?)(?:;q=(1(?:\.0{0,3})?|0(?:\.[0-9]{0,3})))?$/i); + } catch (e) { + do_throw("Invalid language tag or quality value: " + e); + } + + if (i == 0) { + // The first language shouldn't have a quality value. + Assert.equal(qualityValue, undefined); + } else { + let decimalPlaces; + + // When the number of languages is small, we keep the quality value to only one decimal place. + // Otherwise, it can be up to two decimal places. + if (acceptedLanguagesLength < 10) { + Assert.ok(qualityValue.length == 3); + + decimalPlaces = 1; + } else { + Assert.ok(qualityValue.length >= 3); + Assert.ok(qualityValue.length <= 4); + + decimalPlaces = 2; + } + + // All the other languages should have an evenly-spaced quality value. + Assert.equal( + parseFloat(qualityValue).toFixed(decimalPlaces), + (1.0 - (1 / acceptedLanguagesLength) * i).toFixed(decimalPlaces) + ); + } + } +} + +function setupChannel(path) { + let chan = NetUtil.newChannel({ + uri: "http://localhost:4444" + path, + loadUsingSystemPrincipal: true, + }); + + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/test_header_Accept-Language_case.js b/netwerk/test/unit/test_header_Accept-Language_case.js new file mode 100644 index 0000000000..bbad7c9879 --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language_case.js @@ -0,0 +1,53 @@ +"use strict"; + +var testpath = "/bug1054739"; + +function run_test() { + let intlPrefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService) + .getBranch("intl."); + + let oldAcceptLangPref = intlPrefs.getCharPref("accept_languages"); + + let testData = [ + ["en", "en"], + ["ast", "ast"], + ["fr-ca", "fr-CA"], + ["zh-yue", "zh-yue"], + ["az-latn", "az-Latn"], + ["sl-nedis", "sl-nedis"], + ["zh-hant-hk", "zh-Hant-HK"], + ["ZH-HANT-HK", "zh-Hant-HK"], + ["en-us-x-priv", "en-US-x-priv"], + ["en-us-x-twain", "en-US-x-twain"], + ["de, en-US, en", "de,en-US;q=0.7,en;q=0.3"], + ["de,en-us,en", "de,en-US;q=0.7,en;q=0.3"], + ["en-US, en", "en-US,en;q=0.5"], + ["EN-US;q=0.2, EN", "en-US,en;q=0.5"], + ["en ;q=0.8, de ", "en,de;q=0.5"], + [",en,", "en"], + ]; + + for (let i = 0; i < testData.length; i++) { + let acceptLangPref = testData[i][0]; + let expectedHeader = testData[i][1]; + + intlPrefs.setCharPref("accept_languages", acceptLangPref); + let acceptLangHeader = setupChannel(testpath).getRequestHeader( + "Accept-Language" + ); + equal(acceptLangHeader, expectedHeader); + } + + intlPrefs.setCharPref("accept_languages", oldAcceptLangPref); +} + +function setupChannel(path) { + let uri = NetUtil.newURI("http://localhost:4444" + path); + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/test_header_Server_Timing.js b/netwerk/test/unit/test_header_Server_Timing.js new file mode 100644 index 0000000000..c4a3f6e336 --- /dev/null +++ b/netwerk/test/unit/test_header_Server_Timing.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// +// HTTP Server-Timing header test +// + +"use strict"; + +function make_and_open_channel(url, callback) { + let chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true }); + chan.asyncOpen(new ChannelListener(callback, null, CL_ALLOW_UNKNOWN_CL)); +} + +var responseServerTiming = [ + { metric: "metric", duration: "123.4", description: "description" }, + { metric: "metric2", duration: "456.78", description: "description1" }, +]; +var trailerServerTiming = [ + { metric: "metric3", duration: "789.11", description: "description2" }, + { metric: "metric4", duration: "1112.13", description: "description3" }, +]; + +function run_test() { + do_test_pending(); + + // Set up to allow the cert presented by the server + do_get_profile(); + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.dns.localDomains"); + }); + + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + var serverPort = env.get("MOZHTTP2_PORT"); + make_and_open_channel( + "https://foo.example.com:" + serverPort + "/server-timing", + readServerContent + ); +} + +function checkServerTimingContent(headers) { + var expectedResult = responseServerTiming.concat(trailerServerTiming); + Assert.equal(headers.length, expectedResult.length); + + for (var i = 0; i < expectedResult.length; i++) { + let header = headers.queryElementAt(i, Ci.nsIServerTiming); + Assert.equal(header.name, expectedResult[i].metric); + Assert.equal(header.description, expectedResult[i].description); + Assert.equal(header.duration, parseFloat(expectedResult[i].duration)); + } +} + +function readServerContent(request, buffer) { + let channel = request.QueryInterface(Ci.nsITimedChannel); + let headers = channel.serverTiming.QueryInterface(Ci.nsIArray); + checkServerTimingContent(headers); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_headers.js b/netwerk/test/unit/test_headers.js new file mode 100644 index 0000000000..baaacdf17b --- /dev/null +++ b/netwerk/test/unit/test_headers.js @@ -0,0 +1,175 @@ +// +// cleaner HTTP header test infrastructure +// +// tests bugs: 589292, [add more here: see hg log for definitive list] +// +// TO ADD NEW TESTS: +// 1) Increment up 'lastTest' to new number (say, "99") +// 2) Add new test 'handler99' and 'completeTest99' functions. +// 3) If your test should fail the necko channel, set +// test_flags[99] = CL_EXPECT_FAILURE. +// +// TO DEBUG JUST ONE TEST: temporarily change firstTest and lastTest to equal +// the test # you're interested in. +// +// For tests that need duplicate copies of headers to be sent, see +// test_duplicate_headers.js + +"use strict"; + +var firstTest = 1; // set to test of interest when debugging +var lastTest = 4; // set to test of interest when debugging +//////////////////////////////////////////////////////////////////////////////// + +// Note: sets Cc and Ci variables + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = new HttpServer(); +var index = 0; +var nextTest = firstTest; +var test_flags = []; +var testPathBase = "/test_headers"; + +function run_test() { + httpserver.start(-1); + + do_test_pending(); + run_test_number(nextTest); +} + +function runNextTest() { + if (nextTest == lastTest) { + endTests(); + return; + } + nextTest++; + // Make sure test functions exist + if (globalThis["handler" + nextTest] == undefined) { + do_throw("handler" + nextTest + " undefined!"); + } + if (globalThis["completeTest" + nextTest] == undefined) { + do_throw("completeTest" + nextTest + " undefined!"); + } + + run_test_number(nextTest); +} + +function run_test_number(num) { + let testPath = testPathBase + num; + httpserver.registerPathHandler(testPath, globalThis["handler" + num]); + + var channel = setupChannel(testPath); + let flags = test_flags[num]; // OK if flags undefined for test + channel.asyncOpen( + new ChannelListener(globalThis["completeTest" + num], channel, flags) + ); +} + +function setupChannel(url) { + var chan = NetUtil.newChannel({ + uri: URL + url, + loadUsingSystemPrincipal: true, + }); + var httpChan = chan.QueryInterface(Ci.nsIHttpChannel); + return httpChan; +} + +function endTests() { + httpserver.stop(do_test_finished); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 1: test Content-Disposition channel attributes +function handler1(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Disposition", "attachment; filename=foo"); + response.setHeader("Content-Type", "text/plain", false); +} + +function completeTest1(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionFilename, "foo"); + Assert.equal(chan.contentDispositionHeader, "attachment; filename=foo"); + } catch (ex) { + do_throw("error parsing Content-Disposition: " + ex); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 2: no filename +function handler2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "attachment"); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +function completeTest2(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionHeader, "attachment"); + chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 3: filename missing +function handler3(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "attachment; filename="); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +function completeTest3(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_ATTACHMENT); + Assert.equal(chan.contentDispositionHeader, "attachment; filename="); + chan.contentDispositionFilename; // should barf + + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test 4: inline +function handler4(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Disposition", "inline"); + var body = "foo"; + response.bodyOutputStream.write(body, body.length); +} + +function completeTest4(request, data, ctx) { + try { + var chan = request.QueryInterface(Ci.nsIChannel); + Assert.equal(chan.contentDisposition, chan.DISPOSITION_INLINE); + Assert.equal(chan.contentDispositionHeader, "inline"); + + chan.contentDispositionFilename; // should barf + do_throw("Should have failed getting Content-Disposition filename"); + } catch (ex) { + info("correctly ate exception"); + } + runNextTest(); +} diff --git a/netwerk/test/unit/test_hostnameIsLocalIPAddress.js b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js new file mode 100644 index 0000000000..589766b522 --- /dev/null +++ b/netwerk/test/unit/test_hostnameIsLocalIPAddress.js @@ -0,0 +1,41 @@ +"use strict"; + +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +function run_test() { + let testURIs = [ + ["http://example.com", false], + ["about:robots", false], + // 10/8 prefix (RFC 1918) + ["http://9.255.255.255", false], + ["http://10.0.0.0", true], + ["http://10.0.23.31", true], + ["http://10.255.255.255", true], + ["http://11.0.0.0", false], + // 169.254/16 prefix (Link Local) + ["http://169.253.255.255", false], + ["http://169.254.0.0", true], + ["http://169.254.42.91", true], + ["http://169.254.255.255", true], + ["http://169.255.0.0", false], + // 172.16/12 prefix (RFC 1918) + ["http://172.15.255.255", false], + ["http://172.16.0.0", true], + ["http://172.25.110.0", true], + ["http://172.31.255.255", true], + ["http://172.32.0.0", false], + // 192.168/16 prefix (RFC 1918) + ["http://192.167.255.255", false], + ["http://192.168.0.0", true], + ["http://192.168.127.10", true], + ["http://192.168.255.255", true], + ["http://192.169.0.0", false], + ]; + + for (let [uri, isLocal] of testURIs) { + let nsuri = ioService.newURI(uri); + equal(isLocal, ioService.hostnameIsLocalIPAddress(nsuri)); + } +} diff --git a/netwerk/test/unit/test_hostnameIsSharedIPAddress.js b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js new file mode 100644 index 0000000000..80a848bfbb --- /dev/null +++ b/netwerk/test/unit/test_hostnameIsSharedIPAddress.js @@ -0,0 +1,21 @@ +"use strict"; + +var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + Ci.nsIIOService +); + +function run_test() { + let testURIs = [ + // 100.64/10 prefix (RFC 6598) + ["http://100.63.255.254", false], + ["http://100.64.0.0", true], + ["http://100.91.63.42", true], + ["http://100.127.255.254", true], + ["http://100.128.0.0", false], + ]; + + for (let [uri, isShared] of testURIs) { + let nsuri = ioService.newURI(uri); + equal(isShared, ioService.hostnameIsSharedIPAddress(nsuri)); + } +} diff --git a/netwerk/test/unit/test_http1-proxy.js b/netwerk/test/unit/test_http1-proxy.js new file mode 100644 index 0000000000..88faf7d884 --- /dev/null +++ b/netwerk/test/unit/test_http1-proxy.js @@ -0,0 +1,229 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test checks following expectations when using HTTP/1 proxy: + * + * - check we are seeing expected nsresult error codes on channels + * (nsIChannel.status) corresponding to different proxy status code + * responses (502, 504, 407, ...) + * - check we don't try to ask for credentials or otherwise authenticate to + * the proxy when 407 is returned and there is no Proxy-Authenticate + * response header sent + */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +let server_port; +let http_server; + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + if (uri.spec.match(/(\/proxy-session-counter)/)) { + cb.onProxyFilterResult(pi); + return; + } + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + "", + "", + this._flags, + 1000, + null + ) + ); + } +} + +class UnxpectedAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth() { + this.signal.triggered = true; + throw Components.Exception("", Cr.ERROR_UNEXPECTED); + } +} + +class AuthRequestor { + constructor(prompt) { + this.prompt = prompt; + this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]); + } + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this.prompt(); + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); +} + +function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener( + (request, data) => { + request.QueryInterface(Ci.nsIHttpChannel); + const status = request.status; + const http_code = status ? undefined : request.responseStatus; + request.QueryInterface(Ci.nsIProxiedChannel); + const proxy_connect_response_code = + request.httpProxyConnectResponseCode; + resolve({ status, http_code, data, proxy_connect_response_code }); + }, + null, + flags + ) + ); + }); +} + +function connect_handler(request, response) { + Assert.equal(request.method, "CONNECT"); + + switch (request.host) { + case "404.example.com": + response.setStatusLine(request.httpVersion, 404, "Not found"); + break; + case "407.example.com": + response.setStatusLine(request.httpVersion, 407, "Authenticate"); + // And deliberately no Proxy-Authenticate header + break; + case "429.example.com": + response.setStatusLine(request.httpVersion, 429, "Too Many Requests"); + break; + case "502.example.com": + response.setStatusLine(request.httpVersion, 502, "Bad Gateway"); + break; + case "504.example.com": + response.setStatusLine(request.httpVersion, 504, "Gateway timeout"); + break; + default: + response.setStatusLine(request.httpVersion, 500, "I am dumb"); + } +} + +add_task(async function setup() { + http_server = new HttpServer(); + http_server.identity.add("https", "404.example.com", 443); + http_server.identity.add("https", "407.example.com", 443); + http_server.identity.add("https", "429.example.com", 443); + http_server.identity.add("https", "502.example.com", 443); + http_server.identity.add("https", "504.example.com", 443); + http_server.registerPathHandler("CONNECT", connect_handler); + http_server.start(-1); + server_port = http_server.identity.primaryPort; + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + pps.registerFilter(new ProxyFilter("http", "localhost", server_port, 0), 10); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.dns.native-is-localhost"); +}); + +/** + * Test series beginning. + */ + +// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error +// code from the channel and not try to ask for any credentials. +add_task(async function proxy_auth_failure() { + const chan = make_channel(`https://407.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new UnxpectedAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED); + Assert.equal(proxy_connect_response_code, 407); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger"); +}); + +// 502 Bad gateway code returned by the proxy. +add_task(async function proxy_bad_gateway_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); +}); + +// 504 Gateway timeout code returned by the proxy. +add_task(async function proxy_gateway_timeout_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://504.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT); + Assert.equal(proxy_connect_response_code, 504); + Assert.equal(http_code, undefined); +}); + +// 404 Not Found means the proxy could not resolve the host. +add_task(async function proxy_host_not_found_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://404.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(proxy_connect_response_code, 404); + Assert.equal(http_code, undefined); +}); + +// 429 Too Many Requests means we sent too many requests. +add_task(async function proxy_too_many_requests_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://429.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS); + Assert.equal(proxy_connect_response_code, 429); + Assert.equal(http_code, undefined); +}); + +add_task(async function shutdown() { + await new Promise(resolve => { + http_server.stop(resolve); + }); +}); diff --git a/netwerk/test/unit/test_http2-proxy.js b/netwerk/test/unit/test_http2-proxy.js new file mode 100644 index 0000000000..1c9d6151be --- /dev/null +++ b/netwerk/test/unit/test_http2-proxy.js @@ -0,0 +1,615 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This test checks following expectations when using HTTP/2 proxy: + * + * - when we request https access, we don't create different sessions for + * different origins, only new tunnels inside a single session + * - when the isolation key (`proxy_isolation`) is changed, new single session + * is created for new requests to same origins as before + * - error code returned from the tunnel (a proxy error - not end-server + * error!) doesn't kill the existing session + * - check we are seeing expected nsresult error codes on channels + * (nsIChannel.status) corresponding to different proxy status code + * responses (502, 504, 407, ...) + * - check we don't try to ask for credentials or otherwise authenticate to + * the proxy when 407 is returned and there is no Proxy-Authenticate + * response header sent + */ + +/* eslint-env node */ +/* global serverPort */ + +"use strict"; + +const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + +let proxy_port; +let filter; +let proxy; + +// See moz-http2 +const proxy_auth = "authorization-token"; +let proxy_isolation; + +class ProxyFilter { + constructor(type, host, port, flags) { + this._type = type; + this._host = host; + this._port = port; + this._flags = flags; + this.QueryInterface = ChromeUtils.generateQI(["nsIProtocolProxyFilter"]); + } + applyFilter(uri, pi, cb) { + if ( + uri.pathQueryRef.startsWith("/execute") || + uri.pathQueryRef.startsWith("/fork") || + uri.pathQueryRef.startsWith("/kill") + ) { + // So we allow NodeServer.execute to work + cb.onProxyFilterResult(pi); + return; + } + cb.onProxyFilterResult( + pps.newProxyInfo( + this._type, + this._host, + this._port, + proxy_auth, + proxy_isolation, + this._flags, + 1000, + null + ) + ); + } +} + +class UnxpectedAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth() { + this.signal.triggered = true; + throw Components.Exception("", Cr.ERROR_UNEXPECTED); + } +} + +class SimpleAuthPrompt2 { + constructor(signal) { + this.signal = signal; + this.QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt2"]); + } + asyncPromptAuth(channel, callback, context, encryptionLevel, authInfo) { + this.signal.triggered = true; + executeSoon(function() { + authInfo.username = "user"; + authInfo.password = "pass"; + callback.onAuthAvailable(context, authInfo); + }); + } +} + +class AuthRequestor { + constructor(prompt) { + this.prompt = prompt; + this.QueryInterface = ChromeUtils.generateQI(["nsIInterfaceRequestor"]); + } + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2)) { + return this.prompt(); + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } +} + +function make_channel(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); +} + +function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) { + return new Promise(resolve => { + channel.asyncOpen( + new ChannelListener( + (request, data) => { + request.QueryInterface(Ci.nsIHttpChannel); + const status = request.status; + const http_code = status ? undefined : request.responseStatus; + request.QueryInterface(Ci.nsIProxiedChannel); + const proxy_connect_response_code = + request.httpProxyConnectResponseCode; + resolve({ status, http_code, data, proxy_connect_response_code }); + }, + null, + flags + ) + ); + }); +} + +let initial_session_count = 0; + +class http2ProxyCode { + static listen(server, envport) { + if (!server) { + return Promise.resolve(0); + } + + let portSelection = 0; + if (envport !== undefined) { + try { + portSelection = parseInt(envport, 10); + } catch (e) { + portSelection = -1; + } + } + return new Promise(resolve => { + server.listen(portSelection, "0.0.0.0", 2000, () => { + resolve(server.address().port); + }); + }); + } + + static startNewProxy() { + const fs = require("fs"); + const options = { + key: fs.readFileSync(__dirname + "/http2-cert.key"), + cert: fs.readFileSync(__dirname + "/http2-cert.pem"), + }; + const http2 = require("http2"); + global.proxy = http2.createSecureServer(options); + this.setupProxy(); + return http2ProxyCode.listen(proxy).then(port => { + return { port, success: true }; + }); + } + + static closeProxy() { + proxy.closeSockets(); + return new Promise(resolve => { + proxy.close(resolve); + }); + } + + static proxySessionCount() { + if (!proxy) { + return 0; + } + return proxy.proxy_session_count; + } + + static setupProxy() { + if (!proxy) { + throw new Error("proxy is null"); + } + proxy.proxy_session_count = 0; + proxy.on("session", () => { + ++proxy.proxy_session_count; + }); + + // We need to track active connections so we can forcefully close keep-alive + // connections when shutting down the proxy. + proxy.socketIndex = 0; + proxy.socketMap = {}; + proxy.on("connection", function(socket) { + let index = proxy.socketIndex++; + proxy.socketMap[index] = socket; + socket.on("close", function() { + delete proxy.socketMap[index]; + }); + }); + proxy.closeSockets = function() { + for (let i in proxy.socketMap) { + proxy.socketMap[i].destroy(); + } + }; + + proxy.on("stream", (stream, headers) => { + if (headers[":method"] !== "CONNECT") { + // Only accept CONNECT requests + stream.respond({ ":status": 405 }); + stream.end(); + return; + } + + const target = headers[":authority"]; + + const authorization_token = headers["proxy-authorization"]; + if (target == "407.example.com:443") { + stream.respond({ ":status": 407 }); + // Deliberately send no Proxy-Authenticate header + stream.end(); + return; + } + if (target == "407.basic.example.com:443") { + // we want to return a different response than 407 to not re-request + // credentials (and thus loop) but also not 200 to not let the channel + // attempt to waste time connecting a non-existing https server - hence + // 418 I'm a teapot :) + if ("Basic dXNlcjpwYXNz" == authorization_token) { + stream.respond({ ":status": 418 }); + stream.end(); + return; + } + stream.respond({ + ":status": 407, + "proxy-authenticate": "Basic realm='foo'", + }); + stream.end(); + return; + } + if (target == "404.example.com:443") { + // 404 Not Found, a response code that a proxy should return when the host can't be found + stream.respond({ ":status": 404 }); + stream.end(); + return; + } + if (target == "429.example.com:443") { + // 429 Too Many Requests, a response code that a proxy should return when receiving too many requests + stream.respond({ ":status": 429 }); + stream.end(); + return; + } + if (target == "502.example.com:443") { + // 502 Bad Gateway, a response code mostly resembling immediate connection error + stream.respond({ ":status": 502 }); + stream.end(); + return; + } + if (target == "504.example.com:443") { + // 504 Gateway Timeout, did not receive a timely response from an upstream server + stream.respond({ ":status": 504 }); + stream.end(); + return; + } + + const net = require("net"); + const socket = net.connect(serverPort, "127.0.0.1", () => { + try { + stream.respond({ ":status": 200 }); + socket.pipe(stream); + stream.pipe(socket); + } catch (exception) { + console.log(exception); + stream.close(); + } + }); + socket.on("error", error => { + throw `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`; + }); + }); + } +} + +async function proxy_session_counter() { + let data = await NodeServer.execute( + processId, + `http2ProxyCode.proxySessionCount()` + ); + return parseInt(data) - initial_session_count; +} +let processId; +add_task(async function setup() { + // Set to allow the cert presented by our H2 server + do_get_profile(); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + const env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let server_port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(server_port, null); + processId = await NodeServer.fork(); + await NodeServer.execute(processId, `serverPort = ${server_port}`); + await NodeServer.execute(processId, http2ProxyCode); + let proxy = await NodeServer.execute( + processId, + `http2ProxyCode.startNewProxy()` + ); + proxy_port = proxy.port; + Assert.notEqual(proxy_port, null); + + Services.prefs.setStringPref( + "services.settings.server", + `data:text/html,test` + ); + + Services.prefs.setBoolPref("network.http.spdy.enabled", true); + Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true); + + // Even with network state isolation active, we don't end up using the + // partitioned principal. + Services.prefs.setBoolPref("privacy.partition.network_state", true); + + // make all native resolve calls "secretly" resolve localhost instead + Services.prefs.setBoolPref("network.dns.native-is-localhost", true); + + filter = new ProxyFilter("https", "localhost", proxy_port, 0); + pps.registerFilter(filter, 10); + + initial_session_count = await proxy_session_counter(); + info(`Initial proxy session count = ${initial_session_count}`); +}); + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("services.settings.server"); + Services.prefs.clearUserPref("network.http.spdy.enabled"); + Services.prefs.clearUserPref("network.http.spdy.enabled.http2"); + Services.prefs.clearUserPref("network.dns.native-is-localhost"); + + pps.unregisterFilter(filter); + + await NodeServer.execute(processId, `http2ProxyCode.closeProxy()`); + await NodeServer.kill(processId); +}); + +/** + * Test series beginning. + */ + +// Check we reach the h2 end server and keep only one session with the proxy for two different origin. +// Here we use the first isolation token. +add_task(async function proxy_success_one_session() { + proxy_isolation = "TOKEN1"; + + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.ok(alt1.data.match("You Win!")); + Assert.equal( + await proxy_session_counter(), + 1, + "Created just one session with the proxy" + ); +}); + +// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error +// code from the channel and not try to ask for any credentials. +add_task(async function proxy_auth_failure() { + const chan = make_channel(`https://407.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new UnxpectedAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED); + Assert.equal(proxy_connect_response_code, 407); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger"); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 407" + ); +}); + +// The proxy responses with 407 with Proxy-Authenticate header presence. Make +// sure that we prompt the auth prompt to ask for credentials. +add_task(async function proxy_auth_basic() { + const chan = make_channel(`https://407.basic.example.com/`); + const auth_prompt = { triggered: false }; + chan.notificationCallbacks = new AuthRequestor( + () => new SimpleAuthPrompt2(auth_prompt) + ); + const { status, http_code, proxy_connect_response_code } = await get_response( + chan, + CL_EXPECT_FAILURE + ); + + // 418 indicates we pass the basic authentication. + Assert.equal(status, Cr.NS_ERROR_PROXY_CONNECTION_REFUSED); + Assert.equal(proxy_connect_response_code, 418); + Assert.equal(http_code, undefined); + Assert.equal(auth_prompt.triggered, true, "Auth prompt should trigger"); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 407" + ); +}); + +// 502 Bad gateway code returned by the proxy, still one session only, proper different code +// from the channel. +add_task(async function proxy_bad_gateway_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 502 after 407" + ); +}); + +// Second 502 Bad gateway code returned by the proxy, still one session only with the proxy. +add_task(async function proxy_bad_gateway_failure_two() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(proxy_connect_response_code, 502); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by second 502" + ); +}); + +// 504 Gateway timeout code returned by the proxy, still one session only, proper different code +// from the channel. +add_task(async function proxy_gateway_timeout_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://504.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT); + Assert.equal(proxy_connect_response_code, 504); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 504 after 502" + ); +}); + +// 404 Not Found means the proxy could not resolve the host. As for other error responses +// we still expect this not to close the existing session. +add_task(async function proxy_host_not_found_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://404.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST); + Assert.equal(proxy_connect_response_code, 404); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 404 after 504" + ); +}); + +add_task(async function proxy_too_many_requests_failure() { + const { status, http_code, proxy_connect_response_code } = await get_response( + make_channel(`https://429.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(status, Cr.NS_ERROR_PROXY_TOO_MANY_REQUESTS); + Assert.equal(proxy_connect_response_code, 429); + Assert.equal(http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created by 429 after 504" + ); +}); + +// Make sure that the above error codes don't kill the session and we still reach the end server +add_task(async function proxy_success_still_one_session() { + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.http_code, 200); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.equal( + await proxy_session_counter(), + 1, + "No new session created after proxy error codes" + ); +}); + +// Have a new isolation key, this means we are expected to create a new, and again one only, +// session with the proxy to reach the end server. +add_task(async function proxy_success_isolated_session() { + Assert.notEqual(proxy_isolation, "TOKEN2"); + proxy_isolation = "TOKEN2"; + + const foo = await get_response( + make_channel(`https://foo.example.com/random-request-1`) + ); + const alt1 = await get_response( + make_channel(`https://alt1.example.com/random-request-2`) + ); + const lh = await get_response( + make_channel(`https://localhost/random-request-3`) + ); + + Assert.equal(foo.status, Cr.NS_OK); + Assert.equal(foo.proxy_connect_response_code, 200); + Assert.equal(foo.http_code, 200); + Assert.ok(foo.data.match("random-request-1")); + Assert.ok(foo.data.match("You Win!")); + Assert.equal(alt1.status, Cr.NS_OK); + Assert.equal(alt1.proxy_connect_response_code, 200); + Assert.equal(alt1.http_code, 200); + Assert.ok(alt1.data.match("random-request-2")); + Assert.ok(alt1.data.match("You Win!")); + Assert.equal(lh.status, Cr.NS_OK); + Assert.equal(lh.proxy_connect_response_code, 200); + Assert.equal(lh.http_code, 200); + Assert.ok(lh.data.match("random-request-3")); + Assert.ok(lh.data.match("You Win!")); + Assert.equal( + await proxy_session_counter(), + 2, + "Just one new session seen after changing the isolation key" + ); +}); + +// Check that error codes are still handled the same way with new isolation, just in case. +add_task(async function proxy_bad_gateway_failure_isolated() { + const failure1 = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + const failure2 = await get_response( + make_channel(`https://502.example.com/`), + CL_EXPECT_FAILURE + ); + + Assert.equal(failure1.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(failure1.proxy_connect_response_code, 502); + Assert.equal(failure1.http_code, undefined); + Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY); + Assert.equal(failure2.proxy_connect_response_code, 502); + Assert.equal(failure2.http_code, undefined); + Assert.equal( + await proxy_session_counter(), + 2, + "No new session created by 502" + ); +}); diff --git a/netwerk/test/unit/test_http2.js b/netwerk/test/unit/test_http2.js new file mode 100644 index 0000000000..afc47ba740 --- /dev/null +++ b/netwerk/test/unit/test_http2.js @@ -0,0 +1,1442 @@ +// test HTTP/2 + +"use strict"; + +// Generate a small and a large post with known pre-calculated md5 sums +function generateContent(size) { + var content = ""; + for (var i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +var posts = []; +posts.push(generateContent(10)); +posts.push(generateContent(250000)); +posts.push(generateContent(128000)); + +// pre-calculated md5sums (in hex) of the above posts +var md5s = [ + "f1b708bba17f1ce948dc979f4d7092bc", + "2ef8d3b6c8f329318eb1a119b12622b6", +]; + +var bigListenerData = generateContent(128 * 1024); +var bigListenerMD5 = "8f607cfdd2c87d6a7eedb657dafbd836"; + +function checkIsHttp2(request) { + try { + if (request.getResponseHeader("X-Firefox-Spdy") == "h2") { + if (request.getResponseHeader("X-Connection-Http2") == "yes") { + return true; + } + return false; // Weird case, but the server disagrees with us + } + } catch (e) { + // Nothing to do here + } + return false; +} + +var Http2CheckListener = function() {}; + +Http2CheckListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + isHttp2Connection: false, + shouldBeHttp2: true, + accum: 0, + expected: -1, + shouldSucceed: true, + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (this.shouldSucceed && !Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } else if ( + !this.shouldSucceed && + Components.isSuccessCode(request.status) + ) { + do_throw("Channel succeeded unexpectedly!"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.requestSucceeded, this.shouldSucceed); + if (this.shouldSucceed) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + if (this.expected != -1) { + Assert.equal(this.accum, this.expected); + } + if (this.shouldSucceed) { + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + } else { + Assert.ok(!Components.isSuccessCode(status)); + } + + run_next_test(); + do_test_finished(); + }, +}; + +/* + * Support for testing valid multiplexing of streams + */ + +var multiplexContent = generateContent(30 * 1024); +var completed_channels = []; +function register_completed_channel(listener) { + completed_channels.push(listener); + if (completed_channels.length == 2) { + Assert.notEqual( + completed_channels[0].streamID, + completed_channels[1].streamID + ); + run_next_test(); + do_test_finished(); + } +} + +/* Listener class to control the testing of multiplexing */ +var Http2MultiplexListener = function() {}; + +Http2MultiplexListener.prototype = new Http2CheckListener(); + +Http2MultiplexListener.prototype.streamID = 0; +Http2MultiplexListener.prototype.buffer = ""; + +Http2MultiplexListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.streamID = parseInt(request.getResponseHeader("X-Http2-StreamID")); + var data = read_stream(stream, cnt); + this.buffer = this.buffer.concat(data); +}; + +Http2MultiplexListener.prototype.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + Assert.ok(this.buffer == multiplexContent); + + // This is what does most of the hard work for us + register_completed_channel(this); +}; + +// Does the appropriate checks for header gatewaying +var Http2HeaderListener = function(name, callback) { + this.name = name; + this.callback = callback; +}; + +Http2HeaderListener.prototype = new Http2CheckListener(); +Http2HeaderListener.prototype.value = ""; + +Http2HeaderListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + var hvalue = request.getResponseHeader(this.name); + Assert.notEqual(hvalue, ""); + this.callback(hvalue); + read_stream(stream, cnt); +}; + +var Http2PushListener = function(shouldBePushed) { + this.shouldBePushed = shouldBePushed; +}; + +Http2PushListener.prototype = new Http2CheckListener(); + +Http2PushListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/push.js" || + request.originalURI.spec == + "https://localhost:" + serverPort + "/push2.js" || + request.originalURI.spec == "https://localhost:" + serverPort + "/push5.js" + ) { + Assert.equal( + request.getResponseHeader("pushed"), + this.shouldBePushed ? "yes" : "no" + ); + } + read_stream(stream, cnt); +}; + +const pushHdrTxt = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +const pullHdrTxt = pushHdrTxt + .split("") + .reverse() + .join(""); + +function checkContinuedHeaders(getHeader, headerPrefix, headerText) { + for (var i = 0; i < 265; i++) { + Assert.equal(getHeader(headerPrefix + 1), headerText); + } +} + +var Http2ContinuedHeaderListener = function() {}; + +Http2ContinuedHeaderListener.prototype = new Http2CheckListener(); + +Http2ContinuedHeaderListener.prototype.onStopsLeft = 2; + +Http2ContinuedHeaderListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIHttpPushListener", + "nsIStreamListener", +]); + +Http2ContinuedHeaderListener.prototype.getInterface = function(aIID) { + return this.QueryInterface(aIID); +}; + +Http2ContinuedHeaderListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/continuedheaders" + ) { + // This is the original request, so the only one where we'll have continued response headers + checkContinuedHeaders( + request.getResponseHeader, + "X-Pull-Test-Header-", + pullHdrTxt + ); + } + read_stream(stream, cnt); +}; + +Http2ContinuedHeaderListener.prototype.onStopRequest = function( + request, + status +) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + + --this.onStopsLeft; + if (this.onStopsLeft === 0) { + run_next_test(); + do_test_finished(); + } +}; + +Http2ContinuedHeaderListener.prototype.onPush = function( + associatedChannel, + pushChannel +) { + Assert.equal( + associatedChannel.originalURI.spec, + "https://localhost:" + serverPort + "/continuedheaders" + ); + Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true"); + checkContinuedHeaders( + pushChannel.getRequestHeader, + "X-Push-Test-Header-", + pushHdrTxt + ); + + pushChannel.asyncOpen(this); +}; + +// Does the appropriate checks for a large GET response +var Http2BigListener = function() {}; + +Http2BigListener.prototype = new Http2CheckListener(); +Http2BigListener.prototype.buffer = ""; + +Http2BigListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.buffer = this.buffer.concat(read_stream(stream, cnt)); + // We know the server should send us the same data as our big post will be, + // so the md5 should be the same + Assert.equal(bigListenerMD5, request.getResponseHeader("X-Expected-MD5")); +}; + +Http2BigListener.prototype.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + + // Don't want to flood output, so don't use do_check_eq + Assert.ok(this.buffer == bigListenerData); + + run_next_test(); + do_test_finished(); +}; + +var Http2HugeSuspendedListener = function() {}; + +Http2HugeSuspendedListener.prototype = new Http2CheckListener(); +Http2HugeSuspendedListener.prototype.count = 0; + +Http2HugeSuspendedListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.count += cnt; + read_stream(stream, cnt); +}; + +Http2HugeSuspendedListener.prototype.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection); + Assert.equal(this.count, 1024 * 1024 * 1); // 1mb of data expected + run_next_test(); + do_test_finished(); +}; + +// Does the appropriate checks for POSTs +var Http2PostListener = function(expected_md5) { + this.expected_md5 = expected_md5; +}; + +Http2PostListener.prototype = new Http2CheckListener(); +Http2PostListener.prototype.expected_md5 = ""; + +Http2PostListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + Assert.equal( + this.expected_md5, + request.getResponseHeader("X-Calculated-MD5") + ); +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var ResumeStalledChannelListener = function() {}; + +ResumeStalledChannelListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + isHttp2Connection: false, + shouldBeHttp2: true, + resumable: null, + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(request.requestSucceeded, true); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + this.resumable.resume(); + }, +}; + +// test a large download that creates stream flow control and +// confirm we can do another independent stream while the download +// stream is stuck +function test_http2_blocking_download() { + var chan = makeChan("https://localhost:" + serverPort + "/bigdownload"); + var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.initialRwin = 500000; // make the stream.suspend push back in h2 + var listener = new Http2CheckListener(); + listener.expected = 3 * 1024 * 1024; + chan.asyncOpen(listener); + chan.suspend(); + // wait 5 seconds so that stream flow control kicks in and then see if we + // can do a basic transaction (i.e. session not blocked). afterwards resume + // channel + do_timeout(5000, function() { + var simpleChannel = makeChan("https://localhost:" + serverPort + "/"); + var sl = new ResumeStalledChannelListener(); + sl.resumable = chan; + simpleChannel.asyncOpen(sl); + }); +} + +// Make sure we make a HTTP2 connection and both us and the server mark it as such +function test_http2_basic() { + var chan = makeChan("https://localhost:" + serverPort + "/"); + var listener = new Http2CheckListener(); + chan.asyncOpen(listener); +} + +function test_http2_basic_unblocked_dep() { + var chan = makeChan( + "https://localhost:" + serverPort + "/basic_unblocked_dep" + ); + var cos = chan.QueryInterface(Ci.nsIClassOfService); + cos.addClassFlags(Ci.nsIClassOfService.Unblocked); + var listener = new Http2CheckListener(); + chan.asyncOpen(listener); +} + +// make sure we don't use h2 when disallowed +function test_http2_nospdy() { + var chan = makeChan("https://localhost:" + serverPort + "/"); + var listener = new Http2CheckListener(); + var internalChannel = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internalChannel.allowSpdy = false; + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); +} + +// Support for making sure XHR works over SPDY +function checkXhr(xhr) { + if (xhr.readyState != 4) { + return; + } + + Assert.equal(xhr.status, 200); + Assert.equal(checkIsHttp2(xhr), true); + run_next_test(); + do_test_finished(); +} + +// Fires off an XHR request over h2 +function test_http2_xhr() { + var req = new XMLHttpRequest(); + req.open("GET", "https://localhost:" + serverPort + "/", true); + req.addEventListener("readystatechange", function(evt) { + checkXhr(req); + }); + req.send(null); +} + +var concurrent_channels = []; + +var Http2ConcurrentListener = function() {}; + +Http2ConcurrentListener.prototype = new Http2CheckListener(); +Http2ConcurrentListener.prototype.count = 0; +Http2ConcurrentListener.prototype.target = 0; +Http2ConcurrentListener.prototype.reset = 0; +Http2ConcurrentListener.prototype.recvdHdr = 0; + +Http2ConcurrentListener.prototype.onStopRequest = function(request, status) { + this.count++; + Assert.ok(this.isHttp2Connection); + if (this.recvdHdr > 0) { + Assert.equal(request.getResponseHeader("X-Recvd"), this.recvdHdr); + } + + if (this.count == this.target) { + if (this.reset > 0) { + prefs.setIntPref("network.http.spdy.default-concurrent", this.reset); + } + run_next_test(); + do_test_finished(); + } +}; + +function test_http2_concurrent() { + var concurrent_listener = new Http2ConcurrentListener(); + concurrent_listener.target = 201; + concurrent_listener.reset = prefs.getIntPref( + "network.http.spdy.default-concurrent" + ); + prefs.setIntPref("network.http.spdy.default-concurrent", 100); + + for (var i = 0; i < concurrent_listener.target; i++) { + concurrent_channels[i] = makeChan( + "https://localhost:" + serverPort + "/750ms" + ); + concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + concurrent_channels[i].asyncOpen(concurrent_listener); + } +} + +function test_http2_concurrent_post() { + var concurrent_listener = new Http2ConcurrentListener(); + concurrent_listener.target = 8; + concurrent_listener.recvdHdr = posts[2].length; + concurrent_listener.reset = prefs.getIntPref( + "network.http.spdy.default-concurrent" + ); + prefs.setIntPref("network.http.spdy.default-concurrent", 3); + + for (var i = 0; i < concurrent_listener.target; i++) { + concurrent_channels[i] = makeChan( + "https://localhost:" + serverPort + "/750msPost" + ); + concurrent_channels[i].loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = posts[2]; + var uchan = concurrent_channels[i].QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + concurrent_channels[i].requestMethod = "POST"; + concurrent_channels[i].asyncOpen(concurrent_listener); + } +} + +// Test to make sure we get multiplexing right +function test_http2_multiplex() { + var chan1 = makeChan("https://localhost:" + serverPort + "/multiplex1"); + var chan2 = makeChan("https://localhost:" + serverPort + "/multiplex2"); + var listener1 = new Http2MultiplexListener(); + var listener2 = new Http2MultiplexListener(); + chan1.asyncOpen(listener1); + chan2.asyncOpen(listener2); +} + +// Test to make sure we gateway non-standard headers properly +function test_http2_header() { + var chan = makeChan("https://localhost:" + serverPort + "/header"); + var hvalue = "Headers are fun"; + chan.setRequestHeader("X-Test-Header", hvalue, false); + var listener = new Http2HeaderListener("X-Received-Test-Header", function( + received_hvalue + ) { + Assert.equal(received_hvalue, hvalue); + }); + chan.asyncOpen(listener); +} + +// Test to make sure cookies are split into separate fields before compression +function test_http2_cookie_crumbling() { + var chan = makeChan("https://localhost:" + serverPort + "/cookie_crumbling"); + var cookiesSent = ["a=b", "c=d01234567890123456789", "e=f"].sort(); + chan.setRequestHeader("Cookie", cookiesSent.join("; "), false); + var listener = new Http2HeaderListener("X-Received-Header-Pairs", function( + pairsReceived + ) { + var cookiesReceived = JSON.parse(pairsReceived) + .filter(function(pair) { + return pair[0] == "cookie"; + }) + .map(function(pair) { + return pair[1]; + }) + .sort(); + Assert.equal(cookiesReceived.length, cookiesSent.length); + cookiesReceived.forEach(function(cookieReceived, index) { + Assert.equal(cookiesSent[index], cookieReceived); + }); + }); + chan.asyncOpen(listener); +} + +function test_http2_push1() { + var chan = makeChan("https://localhost:" + serverPort + "/push"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push2() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push3() { + var chan = makeChan("https://localhost:" + serverPort + "/push2"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push4() { + var chan = makeChan("https://localhost:" + serverPort + "/push2.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push5() { + var chan = makeChan("https://localhost:" + serverPort + "/push5"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push6() { + var chan = makeChan("https://localhost:" + serverPort + "/push5.js"); + chan.loadGroup = loadGroup; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +// this is a basic test where the server sends a simple document with 2 header +// blocks. bug 1027364 +function test_http2_doubleheader() { + var chan = makeChan("https://localhost:" + serverPort + "/doubleheader"); + var listener = new Http2CheckListener(); + chan.asyncOpen(listener); +} + +// Make sure we handle GETs that cover more than 2 frames properly +function test_http2_big() { + var chan = makeChan("https://localhost:" + serverPort + "/big"); + var listener = new Http2BigListener(); + chan.asyncOpen(listener); +} + +function test_http2_huge_suspended() { + var chan = makeChan("https://localhost:" + serverPort + "/huge"); + var listener = new Http2HugeSuspendedListener(); + chan.asyncOpen(listener); + chan.suspend(); + do_timeout(500, chan.resume); +} + +// Support for doing a POST +function do_post(content, chan, listener, method) { + var stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = content; + + var uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + + chan.requestMethod = method; + + chan.asyncOpen(listener); +} + +// Make sure we can do a simple POST +function test_http2_post() { + var chan = makeChan("https://localhost:" + serverPort + "/post"); + var listener = new Http2PostListener(md5s[0]); + do_post(posts[0], chan, listener, "POST"); +} + +// Make sure we can do a simple PATCH +function test_http2_patch() { + var chan = makeChan("https://localhost:" + serverPort + "/patch"); + var listener = new Http2PostListener(md5s[0]); + do_post(posts[0], chan, listener, "PATCH"); +} + +// Make sure we can do a POST that covers more than 2 frames +function test_http2_post_big() { + var chan = makeChan("https://localhost:" + serverPort + "/post"); + var listener = new Http2PostListener(md5s[1]); + do_post(posts[1], chan, listener, "POST"); +} + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var httpserv = null; +var httpserv2 = null; +var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + +var altsvcClientListener = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, status) { + var isHttp2Connection = checkIsHttp2( + request.QueryInterface(Ci.nsIHttpChannel) + ); + if (!isHttp2Connection) { + dump("/altsvc1 not over h2 yet - retry\n"); + var chan = makeChan( + "http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1" + ).QueryInterface(Ci.nsIHttpChannel); + // we use this header to tell the server to issue a altsvc frame for the + // speficied origin we will use in the next part of the test + chan.setRequestHeader( + "x-redirect-origin", + "http://foo.example.com:" + httpserv2.identity.primaryPort, + false + ); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(altsvcClientListener); + } else { + Assert.ok(isHttp2Connection); + var chan = makeChan( + "http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2" + ).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(altsvcClientListener2); + } + }, +}; + +var altsvcClientListener2 = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function test_onStopR(request, status) { + var isHttp2Connection = checkIsHttp2( + request.QueryInterface(Ci.nsIHttpChannel) + ); + if (!isHttp2Connection) { + dump("/altsvc2 not over h2 yet - retry\n"); + var chan = makeChan( + "http://foo.example.com:" + httpserv2.identity.primaryPort + "/altsvc2" + ).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(altsvcClientListener2); + } else { + Assert.ok(isHttp2Connection); + run_next_test(); + do_test_finished(); + } + }, +}; + +function altsvcHttp1Server(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Alt-Svc", 'h2=":' + serverPort + '"', false); + var body = "this is where a cool kid would write something neat.\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + + var body = '["http://foo.example.com:' + httpserv.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function altsvcHttp1Server2(metadata, response) { + // this server should never be used thanks to an alt svc frame from the + // h2 server.. but in case of some async lag in setting the alt svc route + // up we have it. + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + var body = "hanging.\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK2(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + + var body = + '["http://foo.example.com:' + httpserv2.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} +function test_http2_altsvc() { + var chan = makeChan( + "http://foo.example.com:" + httpserv.identity.primaryPort + "/altsvc1" + ).QueryInterface(Ci.nsIHttpChannel); + chan.asyncOpen(altsvcClientListener); +} + +var Http2PushApiListener = function() {}; + +Http2PushApiListener.prototype = { + checksPending: 9, // 4 onDataAvailable and 5 onStop + + getInterface(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIHttpPushListener", + "nsIStreamListener", + ]), + + // nsIHttpPushListener + onPush: function onPush(associatedChannel, pushChannel) { + Assert.equal( + associatedChannel.originalURI.spec, + "https://localhost:" + serverPort + "/pushapi1" + ); + Assert.equal(pushChannel.getRequestHeader("x-pushed-request"), "true"); + + pushChannel.asyncOpen(this); + if ( + pushChannel.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1/2" + ) { + pushChannel.cancel(Cr.NS_ERROR_ABORT); + } else if ( + pushChannel.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1/3" + ) { + Assert.ok(pushChannel.getRequestHeader("Accept-Encoding").includes("br")); + } + }, + + // normal Channel listeners + onStartRequest: function pushAPIOnStart(request) {}, + + onDataAvailable: function pushAPIOnDataAvailable( + request, + stream, + offset, + cnt + ) { + Assert.notEqual( + request.originalURI.spec, + "https://localhost:" + serverPort + "/pushapi1/2" + ); + + var data = read_stream(stream, cnt); + + if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1" + ) { + Assert.equal(data[0], "0"); + --this.checksPending; + } else if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1/1" + ) { + Assert.equal(data[0], "1"); + --this.checksPending; // twice + } else if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1/3" + ) { + Assert.equal(data[0], "3"); + --this.checksPending; + } else { + Assert.equal(true, false); + } + }, + + onStopRequest: function test_onStopR(request, status) { + if ( + request.originalURI.spec == + "https://localhost:" + serverPort + "/pushapi1/2" + ) { + Assert.equal(request.status, Cr.NS_ERROR_ABORT); + } else { + Assert.equal(request.status, Cr.NS_OK); + } + + --this.checksPending; // 5 times - one for each push plus the pull + if (!this.checksPending) { + run_next_test(); + do_test_finished(); + } + }, +}; + +// pushAPI testcase 1 expects +// 1 to pull /pushapi1 with 0 +// 2 to see /pushapi1/1 with 1 +// 3 to see /pushapi1/1 with 1 (again) +// 4 to see /pushapi1/2 that it will cancel +// 5 to see /pushapi1/3 with 3 with brotli + +function test_http2_pushapi_1() { + var chan = makeChan("https://localhost:" + serverPort + "/pushapi1"); + chan.loadGroup = loadGroup; + var listener = new Http2PushApiListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen(listener); +} + +var WrongSuiteListener = function() {}; +WrongSuiteListener.prototype = new Http2CheckListener(); +WrongSuiteListener.prototype.shouldBeHttp2 = false; +WrongSuiteListener.prototype.onStopRequest = function(request, status) { + prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", true); + prefs.clearUserPref("security.tls.version.max"); + Http2CheckListener.prototype.onStopRequest.call(this); +}; + +// test that we use h1 without the mandatory cipher suite available when +// offering at most tls1.2 +function test_http2_wrongsuite_tls12() { + prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", false); + prefs.setIntPref("security.tls.version.max", 3); + var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite"); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + var listener = new WrongSuiteListener(); + chan.asyncOpen(listener); +} + +// test that we use h2 when offering tls1.3 or higher regardless of if the +// mandatory cipher suite is available +function test_http2_wrongsuite_tls13() { + prefs.setBoolPref("security.ssl3.ecdhe_rsa_aes_128_gcm_sha256", false); + var chan = makeChan("https://localhost:" + serverPort + "/wrongsuite"); + chan.loadFlags = + Ci.nsIRequest.LOAD_FRESH_CONNECTION | + Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + var listener = new WrongSuiteListener(); + listener.shouldBeHttp2 = true; + chan.asyncOpen(listener); +} + +function test_http2_h11required_stream() { + var chan = makeChan( + "https://localhost:" + serverPort + "/h11required_stream" + ); + var listener = new Http2CheckListener(); + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); +} + +function H11RequiredSessionListener() {} +H11RequiredSessionListener.prototype = new Http2CheckListener(); + +H11RequiredSessionListener.prototype.onStopRequest = function(request, status) { + var streamReused = request.getResponseHeader("X-H11Required-Stream-Ok"); + Assert.equal(streamReused, "yes"); + + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + run_next_test(); + do_test_finished(); +}; + +function test_http2_h11required_session() { + var chan = makeChan( + "https://localhost:" + serverPort + "/h11required_session" + ); + var listener = new H11RequiredSessionListener(); + listener.shouldBeHttp2 = false; + chan.asyncOpen(listener); +} + +function test_http2_retry_rst() { + var chan = makeChan("https://localhost:" + serverPort + "/rstonce"); + var listener = new Http2CheckListener(); + chan.asyncOpen(listener); +} + +function test_http2_continuations() { + var chan = makeChan("https://localhost:" + serverPort + "/continuedheaders"); + chan.loadGroup = loadGroup; + var listener = new Http2ContinuedHeaderListener(); + chan.notificationCallbacks = listener; + chan.asyncOpen(listener); +} + +function Http2IllegalHpackValidationListener() {} +Http2IllegalHpackValidationListener.prototype = new Http2CheckListener(); +Http2IllegalHpackValidationListener.prototype.shouldGoAway = false; + +Http2IllegalHpackValidationListener.prototype.onStopRequest = function( + request, + status +) { + var wentAway = request.getResponseHeader("X-Did-Goaway") === "yes"; + Assert.equal(wentAway, this.shouldGoAway); + + Assert.ok(this.onStartRequestFired); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + run_next_test(); + do_test_finished(); +}; + +function Http2IllegalHpackListener() {} +Http2IllegalHpackListener.prototype = new Http2CheckListener(); +Http2IllegalHpackListener.prototype.shouldGoAway = false; + +Http2IllegalHpackListener.prototype.onStopRequest = function(request, status) { + var chan = makeChan( + "https://localhost:" + serverPort + "/illegalhpack_validate" + ); + var listener = new Http2IllegalHpackValidationListener(); + listener.shouldGoAway = this.shouldGoAway; + chan.asyncOpen(listener); +}; + +function test_http2_illegalhpacksoft() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpacksoft"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = false; + listener.shouldSucceed = false; + chan.asyncOpen(listener); +} + +function test_http2_illegalhpackhard() { + var chan = makeChan("https://localhost:" + serverPort + "/illegalhpackhard"); + var listener = new Http2IllegalHpackListener(); + listener.shouldGoAway = true; + listener.shouldSucceed = false; + chan.asyncOpen(listener); +} + +function test_http2_folded_header() { + var chan = makeChan("https://localhost:" + serverPort + "/foldedheader"); + chan.loadGroup = loadGroup; + var listener = new Http2CheckListener(); + listener.shouldSucceed = false; + chan.asyncOpen(listener); +} + +function test_http2_empty_data() { + var chan = makeChan("https://localhost:" + serverPort + "/emptydata"); + var listener = new Http2CheckListener(); + chan.asyncOpen(listener); +} + +function test_http2_push_firstparty1() { + var chan = makeChan("https://localhost:" + serverPort + "/push"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" }; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push_firstparty2() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "bar.com" }; + var listener = new Http2PushListener(false); + chan.asyncOpen(listener); +} + +function test_http2_push_firstparty3() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { firstPartyDomain: "foo.com" }; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push_userContext1() { + var chan = makeChan("https://localhost:" + serverPort + "/push"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 1 }; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_push_userContext2() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 2 }; + var listener = new Http2PushListener(false); + chan.asyncOpen(listener); +} + +function test_http2_push_userContext3() { + var chan = makeChan("https://localhost:" + serverPort + "/push.js"); + chan.loadGroup = loadGroup; + chan.loadInfo.originAttributes = { userContextId: 1 }; + var listener = new Http2PushListener(true); + chan.asyncOpen(listener); +} + +function test_http2_status_phrase() { + var chan = makeChan("https://localhost:" + serverPort + "/statusphrase"); + var listener = new Http2CheckListener(); + listener.shouldSucceed = false; + chan.asyncOpen(listener); +} + +var PulledDiskCacheListener = function() {}; +PulledDiskCacheListener.prototype = new Http2CheckListener(); +PulledDiskCacheListener.prototype.EXPECTED_DATA = "this was pulled via h2"; +PulledDiskCacheListener.prototype.readData = ""; +PulledDiskCacheListener.prototype.onDataAvailable = function testOnDataAvailable( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.isHttp2Connection = checkIsHttp2(request); + this.accum += cnt; + this.readData += read_stream(stream, cnt); +}; +PulledDiskCacheListener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(this.EXPECTED_DATA, this.readData); + Http2CheckListener.prorotype.onStopRequest.call(this, request, status); +}; + +const DISK_CACHE_DATA = "this is from disk cache"; + +var FromDiskCacheListener = function() {}; +FromDiskCacheListener.prototype = { + onStartRequestFired: false, + onDataAvailableFired: false, + readData: "", + + onStartRequest: function testOnStartRequest(request) { + this.onStartRequestFired = true; + if (!Components.isSuccessCode(request.status)) { + do_throw("Channel should have a success code! (" + request.status + ")"); + } + + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.ok(request.requestSucceeded); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.readData += read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.equal(this.readData, DISK_CACHE_DATA); + + evict_cache_entries("disk"); + syncWithCacheIOThread(() => { + // Now that we know the entry is out of the disk cache, check to make sure + // we don't have this hiding in the push cache somewhere - if we do, it + // didn't get cancelled, and we have a bug. + var chan = makeChan("https://localhost:" + serverPort + "/diskcache"); + var listener = new PulledDiskCacheListener(); + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); + }); + }, +}; + +var Http2DiskCachePushListener = function() {}; + +Http2DiskCachePushListener.prototype = new Http2CheckListener(); + +Http2DiskCachePushListener.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + // Now we need to open a channel to ensure we get data from the disk cache + // for the pushed item, instead of from the push cache. + var chan = makeChan("https://localhost:" + serverPort + "/diskcache"); + var listener = new FromDiskCacheListener(); + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); +}; + +function continue_test_http2_disk_cache_push(status, entry, appCache) { + // TODO - store stuff in cache entry, then open an h2 channel that will push + // this, once that completes, open a channel for the cache entry we made and + // ensure it came from disk cache, not the push cache. + var outputStream = entry.openOutputStream(0, -1); + outputStream.write(DISK_CACHE_DATA, DISK_CACHE_DATA.length); + + // Now we open our URL that will push data for the URL above + var chan = makeChan("https://localhost:" + serverPort + "/pushindisk"); + var listener = new Http2DiskCachePushListener(); + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); +} + +function test_http2_disk_cache_push() { + asyncOpenCacheEntry( + "https://localhost:" + serverPort + "/diskcache", + "disk", + Ci.nsICacheStorage.OPEN_NORMALLY, + null, + continue_test_http2_disk_cache_push, + false + ); +} + +function test_complete() { + resetPrefs(); + do_test_pending(); + httpserv.stop(do_test_finished); + do_test_pending(); + httpserv2.stop(do_test_finished); + + do_test_finished(); + do_timeout(0, run_next_test); +} + +var Http2DoublepushListener = function() {}; +Http2DoublepushListener.prototype = new Http2CheckListener(); +Http2DoublepushListener.prototype.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.ok(this.isHttp2Connection == this.shouldBeHttp2); + + var chan = makeChan("https://localhost:" + serverPort + "/doublypushed"); + var listener = new Http2DoublypushedListener(); + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); +}; + +var Http2DoublypushedListener = function() {}; +Http2DoublypushedListener.prototype = new Http2CheckListener(); +Http2DoublypushedListener.prototype.readData = ""; +Http2DoublypushedListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.accum += cnt; + this.readData += read_stream(stream, cnt); +}; +Http2DoublypushedListener.prototype.onStopRequest = function(request, status) { + Assert.ok(this.onStartRequestFired); + Assert.ok(Components.isSuccessCode(status)); + Assert.ok(this.onDataAvailableFired); + Assert.equal(this.readData, "pushed"); + + run_next_test(); + do_test_finished(); +}; + +function test_http2_doublepush() { + var chan = makeChan("https://localhost:" + serverPort + "/doublepush"); + var listener = new Http2DoublepushListener(); + chan.loadGroup = loadGroup; + chan.asyncOpen(listener); +} + +// hack - the header test resets the multiplex object on the server, +// so make sure header is always run before the multiplex test. +// +// make sure post_big runs first to test race condition in restarting +// a stalled stream when a SETTINGS frame arrives +var tests = [ + test_http2_post_big, + test_http2_basic, + test_http2_concurrent, + test_http2_concurrent_post, + test_http2_basic_unblocked_dep, + test_http2_nospdy, + test_http2_push1, + test_http2_push2, + test_http2_push3, + test_http2_push4, + test_http2_push5, + test_http2_push6, + test_http2_altsvc, + test_http2_doubleheader, + test_http2_xhr, + test_http2_header, + test_http2_cookie_crumbling, + test_http2_multiplex, + test_http2_big, + test_http2_huge_suspended, + test_http2_post, + test_http2_patch, + test_http2_pushapi_1, + test_http2_continuations, + test_http2_blocking_download, + test_http2_illegalhpacksoft, + test_http2_illegalhpackhard, + test_http2_folded_header, + test_http2_empty_data, + test_http2_status_phrase, + test_http2_doublepush, + test_http2_disk_cache_push, + // Add new tests above here - best to add new tests before h1 + // streams get too involved + // These next two must always come in this order + test_http2_h11required_stream, + test_http2_h11required_session, + test_http2_retry_rst, + test_http2_wrongsuite_tls12, + test_http2_wrongsuite_tls13, + test_http2_push_firstparty1, + test_http2_push_firstparty2, + test_http2_push_firstparty3, + test_http2_push_userContext1, + test_http2_push_userContext2, + test_http2_push_userContext3, + + // cleanup + test_complete, +]; +var current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + do_test_pending(); + } +} + +var prefs; +var spdypref; +var spdypush; +var http2pref; +var altsvcpref1; +var altsvcpref2; +var loadGroup; +var serverPort; +var speculativeLimit; + +function resetPrefs() { + prefs.setIntPref("network.http.speculative-parallel-limit", speculativeLimit); + prefs.setBoolPref("network.http.spdy.enabled", spdypref); + prefs.setBoolPref("network.http.spdy.allow-push", spdypush); + prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref); + prefs.setBoolPref("network.http.altsvc.enabled", altsvcpref1); + prefs.setBoolPref("network.http.altsvc.oe", altsvcpref2); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.cookieJarSettings.unblocked_for_testing"); +} + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + serverPort = env.get("MOZHTTP2_PORT"); + Assert.notEqual(serverPort, null); + dump("using port " + serverPort + "\n"); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + speculativeLimit = prefs.getIntPref( + "network.http.speculative-parallel-limit" + ); + prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. Some older tests in + // this suite use localhost with a TOFU exception, but new ones should use + // foo.example.com + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + // Enable all versions of spdy to see that we auto negotiate http/2 + spdypref = prefs.getBoolPref("network.http.spdy.enabled"); + spdypush = prefs.getBoolPref("network.http.spdy.allow-push"); + http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2"); + altsvcpref1 = prefs.getBoolPref("network.http.altsvc.enabled"); + altsvcpref2 = prefs.getBoolPref("network.http.altsvc.oe", true); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.allow-push", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setBoolPref("network.http.altsvc.enabled", true); + prefs.setBoolPref("network.http.altsvc.oe", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + prefs.setBoolPref("network.cookieJarSettings.unblocked_for_testing", true); + + loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + httpserv = new HttpServer(); + httpserv.registerPathHandler("/altsvc1", altsvcHttp1Server); + httpserv.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.example.com", + httpserv.identity.primaryPort + ); + + httpserv2 = new HttpServer(); + httpserv2.registerPathHandler("/altsvc2", altsvcHttp1Server2); + httpserv2.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK2); + httpserv2.start(-1); + httpserv2.identity.setPrimary( + "http", + "foo.example.com", + httpserv2.identity.primaryPort + ); + + // And make go! + run_next_test(); +} diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js new file mode 100644 index 0000000000..dcadde3c6b --- /dev/null +++ b/netwerk/test/unit/test_http3.js @@ -0,0 +1,569 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Generate a post with known pre-calculated md5 sum. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +let post = generateContent(10); + +// Max concurent stream number in neqo is 100. +// Openning 120 streams will test queuing of streams. +let number_of_parallel_requests = 120; +let h1Server = null; +let h3Route; +let httpsOrigin; +let httpOrigin; +let h3AltSvc; + +let prefs; + +let tests = [ + // This test must be the first because it setsup alt-svc connection, that + // other tests use. + test_https_alt_svc, + test_multiple_requests, + test_request_cancelled_by_server, + test_stream_cancelled_by_necko, + test_multiple_request_one_is_cancelled, + test_multiple_request_one_is_cancelled_by_necko, + test_post, + test_patch, + test_http_alt_svc, + test_slow_receiver, + // This test should be at the end, because it will close http3 + // connection and the transaction will switch to already existing http2 + // connection. + // TODO: Bug 1582667 should try to fix issue with connection being closed. + test_version_fallback, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.http3.enabled", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + h1Server = new HttpServer(); + h1Server.registerPathHandler("/http3-test", h1Response); + h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK); + h1Server.registerPathHandler("/VersionFallback", h1Response); + h1Server.start(-1); + h1Server.identity.setPrimary( + "http", + "foo.example.com", + h1Server.identity.primaryPort + ); + httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/"; + + run_next_test(); +} + +function h1Response(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + try { + let hval = "h3-27=" + metadata.getHeader("x-altsvc"); + response.setHeader("Alt-Svc", hval, false); + } catch (e) {} + + let body = "Q: What did 0 say to 8? A: Nice Belt!\n"; + response.bodyOutputStream.write(body, body.length); +} + +function h1ServerWK(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "application/json", false); + response.setHeader("Connection", "close", false); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.setHeader("Access-Control-Allow-Method", "GET", false); + response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false); + + let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]'; + response.bodyOutputStream.write(body, body.length); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3CheckListener = function() {}; + +Http3CheckListener.prototype = { + onDataAvailableFired: false, + expectedStatus: Cr.NS_OK, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, this.expectedStatus); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + Assert.equal(this.onDataAvailableFired, true); + } + run_next_test(); + do_test_finished(); + }, +}; + +let WaitForHttp3Listener = function() {}; + +WaitForHttp3Listener.prototype = new Http3CheckListener(); + +WaitForHttp3Listener.prototype.uri = ""; +WaitForHttp3Listener.prototype.h3AltSvc = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(status, this.expectedStatus); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + + if (routed == this.expectedRoute) { + Assert.equal(routed, this.expectedRoute); // always true, but a useful log + Assert.equal(httpVersion, "h3"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + if (httpVersion == "h2") { + request.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.ok(request.supportsHTTP3); + } + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-27=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + do_test_pending(); + doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc); +} + +// Listener for a number of parallel requests. if with_error is set, one of +// the channels will be cancelled (by the server or in onStartRequest). +let MultipleListener = function() {}; + +MultipleListener.prototype = { + number_of_parallel_requests: 0, + with_error: Cr.NS_OK, + count_of_done_requests: 0, + error_found_onstart: false, + error_found_onstop: false, + need_cancel_found: false, + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + let need_cancel = ""; + try { + need_cancel = request.getRequestHeader("CancelMe"); + } catch (e) {} + if (need_cancel != "") { + this.need_cancel_found = true; + request.cancel(Cr.NS_ERROR_ABORT); + } else if (Components.isSuccessCode(request.status)) { + Assert.equal(request.responseStatus, 200); + } else if (this.error_found_onstart) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstart = true; + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = ""; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(request.status)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + } + + if (!Components.isSuccessCode(request.status)) { + if (this.error_found_onstop) { + do_throw("We should have only one request failing."); + } else { + Assert.equal(request.status, this.with_error); + this.error_found_onstop = true; + } + } + this.count_of_done_requests++; + if (this.count_of_done_requests == this.number_of_parallel_requests) { + if (Components.isSuccessCode(this.with_error)) { + Assert.equal(this.error_found_onstart, false); + Assert.equal(this.error_found_onstop, false); + } else { + Assert.ok(this.error_found_onstart || this.need_cancel_found); + Assert.equal(this.error_found_onstop, true); + } + run_next_test(); + } + do_test_finished(); + }, +}; + +// Multiple requests +function test_multiple_requests() { + dump("test_multiple_requests()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// A request cancelled by a server. +function test_request_cancelled_by_server() { + dump("test_request_cancelled_by_server()\n"); + + let listener = new Http3CheckListener(); + listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "RequestCancelled"); + chan.asyncOpen(listener); + do_test_pending(); +} + +let CancelRequestListener = function() {}; + +CancelRequestListener.prototype = new Http3CheckListener(); + +CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT; + +CancelRequestListener.prototype.onStartRequest = function testOnStartRequest( + request +) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(Components.isSuccessCode(request.status), true); + request.cancel(Cr.NS_ERROR_ABORT); +}; + +// Cancel stream after OnStartRequest. +function test_stream_cancelled_by_necko() { + dump("test_stream_cancelled_by_necko()\n"); + + let listener = new CancelRequestListener(); + listener.expectedRoute = h3Route; + let chan = makeChan(httpsOrigin + "20000"); + chan.asyncOpen(listener); + do_test_pending(); +} + +// Multiple requests, one gets cancelled by the server, the other should finish normally. +function test_multiple_request_one_is_cancelled() { + dump("test_multiple_request_one_is_cancelled()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_NET_INTERRUPT; + listener.expectedRoute = h3Route; + + for (let i = 0; i < number_of_parallel_requests; i++) { + let uri = httpsOrigin + "20000"; + if (i == 4) { + // Add a request that will be cancelled by the server. + uri = httpsOrigin + "RequestCancelled"; + } + let chan = makeChan(uri); + chan.asyncOpen(listener); + do_test_pending(); + } +} + +// Multiple requests, one gets cancelled by us, the other should finish normally. +function test_multiple_request_one_is_cancelled_by_necko() { + dump("test_multiple_request_one_is_cancelled_by_necko()\n"); + + let listener = new MultipleListener(); + listener.number_of_parallel_requests = number_of_parallel_requests; + listener.with_error = Cr.NS_ERROR_ABORT; + listener.expectedRoute = h3Route; + for (let i = 0; i < number_of_parallel_requests; i++) { + let chan = makeChan(httpsOrigin + "20000"); + if (i == 4) { + // MultipleListener will cancel request with this header. + chan.setRequestHeader("CancelMe", "true", false); + } + chan.asyncOpen(listener); + do_test_pending(); + } +} + +let PostListener = function() {}; + +PostListener.prototype = new Http3CheckListener(); + +PostListener.prototype.onDataAvailable = function(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); +}; + +// Support for doing a POST +function do_post(content, chan, listener, method) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = content; + + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + + chan.requestMethod = method; + + chan.asyncOpen(listener); +} + +// Test a simple POST +function test_post() { + dump("test_post()"); + let chan = makeChan(httpsOrigin + "post"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "POST"); + do_test_pending(); +} + +// Test a simple PATCH +function test_patch() { + dump("test_patch()"); + let chan = makeChan(httpsOrigin + "patch"); + let listener = new PostListener(); + listener.expectedRoute = h3Route; + do_post(post, chan, listener, "PATCH"); + do_test_pending(); +} + +// Test alt-svc for http (without s) +function test_http_alt_svc() { + dump("test_http_alt_svc()\n"); + + do_test_pending(); + doTest(httpOrigin + "http3-test", h3Route, h3AltSvc); +} + +let SlowReceiverListener = function() {}; + +SlowReceiverListener.prototype = new Http3CheckListener(); +SlowReceiverListener.prototype.count = 0; + +SlowReceiverListener.prototype.onDataAvailable = function( + request, + stream, + off, + cnt +) { + this.onDataAvailableFired = true; + this.count += cnt; + read_stream(stream, cnt); +}; + +SlowReceiverListener.prototype.onStopRequest = function(request, status) { + Assert.equal(status, this.expectedStatus); + Assert.equal(this.count, 10000000); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + if (Components.isSuccessCode(this.expectedStatus)) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + Assert.equal(this.onDataAvailableFired, true); + } + run_next_test(); + do_test_finished(); +}; + +function test_slow_receiver() { + dump("test_slow_receiver()\n"); + let chan = makeChan(httpsOrigin + "10000000"); + let listener = new SlowReceiverListener(); + listener.expectedRoute = h3Route; + chan.asyncOpen(listener); + do_test_pending(); + chan.suspend(); + do_timeout(1000, chan.resume); +} + +let CheckFallbackListener = function() {}; + +CheckFallbackListener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, "0"); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "http/1.1"); + run_next_test(); + do_test_finished(); + }, +}; + +// Server cancels request with VersionFallback. +function test_version_fallback() { + dump("test_version_fallback()\n"); + + let chan = makeChan(httpsOrigin + "VersionFallback"); + let listener = new CheckFallbackListener(); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); + do_test_pending(); + h1Server.stop(do_test_finished); +} diff --git a/netwerk/test/unit/test_http3_421.js b/netwerk/test/unit/test_http3_421.js new file mode 100644 index 0000000000..65853b1091 --- /dev/null +++ b/netwerk/test/unit/test_http3_421.js @@ -0,0 +1,175 @@ +"use strict"; + +let h3Route; +let httpsOrigin; +let h3AltSvc; +let prefs; + +let tests = [test_https_alt_svc, test_response_421, testsDone]; +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.http3.enabled", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3Listener = function() {}; + +Http3Listener.prototype = { + onDataAvailableFired: false, + buffer: "", + routed: "", + httpVersion: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + this.buffer = this.buffer.concat(read_stream(stream, cnt)); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + Assert.equal(this.onDataAvailableFired, true); + this.routed = "NA"; + try { + this.routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + this.routed + "\n"); + + this.httpVersion = ""; + try { + this.httpVersion = request.protocolVersion; + } catch (e) {} + dump("httpVersion is " + this.httpVersion + "\n"); + }, +}; + +let WaitForHttp3Listener = function() {}; + +WaitForHttp3Listener.prototype = new Http3Listener(); + +WaitForHttp3Listener.prototype.uri = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Http3Listener.prototype.onStopRequest.call(this, request, status); + + if (this.routed == h3Route) { + Assert.equal(this.httpVersion, "h3"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri); + }); + } + + do_test_finished(); +}; + +function doTest(uri) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + chan.setRequestHeader("x-altsvc", h3AltSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-27=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + + do_test_pending(); + doTest(httpsOrigin + "http3-test"); +} + +let Resp421Listener = function() {}; + +Resp421Listener.prototype = new Http3Listener(); + +Resp421Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Http3Listener.prototype.onStopRequest.call(this, request, status); + + Assert.equal(this.routed, "0"); + Assert.equal(this.httpVersion, "h2"); + Assert.ok(this.buffer.match("You Win! [(]by requesting/Response421[)]")); + + run_next_test(); + do_test_finished(); +}; + +function test_response_421() { + dump("test_response_421()\n"); + + let listener = new Resp421Listener(); + let chan = makeChan(httpsOrigin + "Response421"); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); + do_test_pending(); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_http3_alt_svc.js b/netwerk/test/unit/test_http3_alt_svc.js new file mode 100644 index 0000000000..77e7498a9c --- /dev/null +++ b/netwerk/test/unit/test_http3_alt_svc.js @@ -0,0 +1,129 @@ +"use strict"; + +let httpsOrigin; +let h3AltSvc; +let h3Port; +let h3Route; +let prefs; + +let tests = [test_https_alt_svc, testsDone]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.http3.enabled", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let WaitForHttp3Listener = function() {}; + +WaitForHttp3Listener.prototype = { + onDataAvailableFired: false, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); + }, +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h2=foo2.example.com:8000,h3-27=:h3port,h3-29=foo2.example.com:8443 +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + do_test_pending(); + doTest(httpsOrigin + "http3-test2", h3Route, h3AltSvc); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); +} diff --git a/netwerk/test/unit/test_http3_error_before_connect.js b/netwerk/test/unit/test_http3_error_before_connect.js new file mode 100644 index 0000000000..fd1fdaf543 --- /dev/null +++ b/netwerk/test/unit/test_http3_error_before_connect.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish() { + resolve(); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + let h3Port = env.get("MOZHTTP3_PORT_NO_RESPONSE"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=:" + h3Port + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 5); +}); + +let CheckOnlyHttp2Listener = function() {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.ok(routed === "0" || routed === "NA"); + this.finish(); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_fast_fallback.js b/netwerk/test/unit/test_http3_fast_fallback.js new file mode 100644 index 0000000000..9a2d78847b --- /dev/null +++ b/netwerk/test/unit/test_http3_fast_fallback.js @@ -0,0 +1,597 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +let prefs; +let h2Port; +let h3Port; +let listen; +let trrServer; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + h3Port = env.get("MOZHTTP3_PORT_NO_RESPONSE"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.security.esni.enabled", false); + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.security.esni.enabled"); + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + prefs.clearUserPref("network.dns.echconfig.enabled"); + prefs.clearUserPref("network.dns.echconfig.fallback_to_origin"); + prefs.clearUserPref("network.dns.httpssvc.reset_exclustion_list"); + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.httpssvc.http3_fast_fallback_timeout"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + Services.prefs.clearUserPref("network.http.speculative-parallel-limit"); + if (trrServer) { + await trrServer.stop(); + } +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +let CheckOnlyHttp2Listener = function() {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + Assert.ok(routed === "0" || routed === "NA"); + this.finish(); + }, +}; + +async function fast_fallback_test() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(`https://foo.example.com:${h2Port}/`); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 3); +} + +// Test the case when speculative connection is enabled. In this case, when the +// backup connection is ready, the http transaction is still in pending +// queue because the h3 connection is never ready to accept transactions. +add_task(async function test_fast_fallback_with_speculative_connection() { + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=:" + h3Port + ); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + + await fast_fallback_test(); +}); + +let HTTPObserver = { + observeActivity( + aChannel, + aType, + aSubtype, + aTimestamp, + aSizeData, + aStringData + ) { + aChannel.QueryInterface(Ci.nsIChannel); + if (aChannel.URI.spec == `https://foo.example.com:${h2Port}/`) { + if ( + aType == Ci.nsIHttpActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && + aSubtype == + Ci.nsIHttpActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER + ) { + // We need to enable speculative connection again, since the backup + // connection is done by using speculative connection. + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 6); + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + observerService.removeObserver(HTTPObserver); + } + } + }, +}; + +// Test the case when speculative connection is disabled. In this case, when the +// back connection is ready, the http transaction is already activated, +// but the socket is not ready to write. +add_task(async function test_fast_fallback_without_speculative_connection() { + // Make sure the h3 connection created by the previous test is cleared. + Services.obs.notifyObservers(null, "net:cancel-all-connections"); + await new Promise(resolve => setTimeout(resolve, 1000)); + // Clear the h3 excluded list, otherwise the Alt-Svc mapping will not be used. + Services.obs.notifyObservers(null, "network:reset-http3-excluded-list"); + Services.prefs.setIntPref("network.http.speculative-parallel-limit", 0); + + let observerService = Cc[ + "@mozilla.org/network/http-activity-distributor;1" + ].getService(Ci.nsIHttpActivityDistributor); + observerService.addObserver(HTTPObserver); + + await fast_fallback_test(); + + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); +}); + +// Test when echConfig is disabled and we have https rr for http3. We use a +// longer timeout in this test, so when fast fallback timer is triggered, the +// http transaction is already activated. +add_task(async function testFastfallback() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enabled", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1000 + ); + + await trrServer.registerDoHAnswers("test.fastfallback.com", "HTTPS", [ + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fastfallback1.com", + values: [ + { key: "alpn", value: "h3-27" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fastfallback.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fastfallback2.com", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("test.fastfallback1.com", "A", [ + { + name: "test.fastfallback1.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + await trrServer.registerDoHAnswers("test.fastfallback2.com", "A", [ + { + name: "test.fastfallback2.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let chan = makeChan(`https://test.fastfallback.com/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Like the previous test, but with a shorter timeout, so when fast fallback +// timer is triggered, the http transaction is still in pending queue. +add_task(async function testFastfallback1() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enabled", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 10 + ); + + await trrServer.registerDoHAnswers("test.fastfallback.org", "HTTPS", [ + { + name: "test.fastfallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.fastfallback1.org", + values: [ + { key: "alpn", value: "h3-27" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.fastfallback.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.fastfallback2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("test.fastfallback1.org", "A", [ + { + name: "test.fastfallback1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + await trrServer.registerDoHAnswers("test.fastfallback2.org", "A", [ + { + name: "test.fastfallback2.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let chan = makeChan(`https://test.fastfallback.org/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Test when echConfig is enabled, we can sucessfully fallback to the last +// record. +add_task(async function testFastfallbackWithEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enabled", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1000 + ); + + await trrServer.registerDoHAnswers("test.ech.org", "HTTPS", [ + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.ech1.org", + values: [ + { key: "alpn", value: "h3-27" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.ech2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.ech3.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("test.ech1.org", "A", [ + { + name: "test.ech1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + await trrServer.registerDoHAnswers("test.ech3.org", "A", [ + { + name: "test.ech3.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let chan = makeChan(`https://test.ech.org/server-timing`); + let [req] = await channelOpenPromise(chan); + Assert.equal(req.protocolVersion, "h2"); + let internal = req.QueryInterface(Ci.nsIHttpChannelInternal); + Assert.equal(internal.remotePort, h2Port); + + await trrServer.stop(); +}); + +// Test when echConfig is enabled, the connection should fail when not all +// records have echConfig. +add_task(async function testFastfallbackWithpartialEchConfig() { + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + Services.prefs.setBoolPref("network.http.http3.enabled", true); + + Services.prefs.setIntPref( + "network.dns.httpssvc.http3_fast_fallback_timeout", + 1000 + ); + + await trrServer.registerDoHAnswers("test.partial_ech.org", "HTTPS", [ + { + name: "test.partial_ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.partial_ech1.org", + values: [ + { key: "alpn", value: "h3-27" }, + { key: "port", value: h3Port }, + { key: "echconfig", value: "456..." }, + ], + }, + }, + { + name: "test.partial_ech.org", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.partial_ech2.org", + values: [ + { key: "alpn", value: "h2" }, + { key: "port", value: h2Port }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("test.partial_ech1.org", "A", [ + { + name: "test.partial_ech1.org", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let chan = makeChan(`https://test.partial_ech.org/server-timing`); + await channelOpenPromise(chan, CL_EXPECT_LATE_FAILURE | CL_ALLOW_UNKNOWN_CL); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_http3_fatal_stream_error.js b/netwerk/test/unit/test_http3_fatal_stream_error.js new file mode 100644 index 0000000000..48b194fbba --- /dev/null +++ b/netwerk/test/unit/test_http3_fatal_stream_error.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +let Http3FailedListener = function() {}; + +Http3FailedListener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.amount += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + if (Components.isSuccessCode(status)) { + // This is still HTTP2 connection + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(false); + } else { + Assert.equal(status, Cr.NS_ERROR_NET_PARTIAL_TRANSFER); + this.finish(true); + } + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +add_task(async function test_fatal_error() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + + let h3Port = env.get("MOZHTTP3_PORT_FAILED"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=:" + h3Port + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = false; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new Http3FailedListener(); + result = await altsvcSetupPromise(chan, listener); + } while (result === false); +}); + +let CheckOnlyHttp2Listener = function() {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(false); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_large_post.js b/netwerk/test/unit/test_http3_large_post.js new file mode 100644 index 0000000000..e309c2c00d --- /dev/null +++ b/netwerk/test/unit/test_http3_large_post.js @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests(); +}); + +let Http3Listener = function() {}; + +Http3Listener.prototype = { + expectedStatus: Cr.NS_OK, + amount: 0, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + Assert.equal( + this.amount, + request.getResponseHeader("x-data-received-length") + ); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri, amount) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.data = generateContent(amount); + let uchan = chan.QueryInterface(Ci.nsIUploadChannel); + uchan.setUploadStream(stream, "text/plain", stream.available()); + chan.requestMethod = "POST"; + return chan; +} + +// Generate a post. +function generateContent(size) { + let content = ""; + for (let i = 0; i < size; i++) { + content += "0"; + } + return content; +} + +// Send a large post that can fit into a neqo stream buffer at once. +// But the amount of data is larger than the necko's default stream +// buffer side, therefore ReadSegments will be called multiple times. +add_task(async function test_large_post() { + let amount = 1 << 16; + let listener = new Http3Listener(); + listener.amount = amount; + let chan = makeChan("https://foo.example.com/post", amount); + await chanPromise(chan, listener); +}); + +// Send a large post that cannot fit into a neqo stream buffer at once. +// StreamWritable events will trigger sending more data when the buffer +// space is freed +add_task(async function test_large_post2() { + let amount = 1 << 23; + let listener = new Http3Listener(); + listener.amount = amount; + let chan = makeChan("https://foo.example.com/post", amount); + await chanPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_perf.js b/netwerk/test/unit/test_http3_perf.js new file mode 100644 index 0000000000..679450a163 --- /dev/null +++ b/netwerk/test/unit/test_http3_perf.js @@ -0,0 +1,262 @@ +"use strict"; + +var perfMetadata = { + owner: "Network Team", + name: "http3 raw", + description: + "XPCShell tests that verifies the lib integration against a local server", + options: { + default: { + perfherder: true, + perfherder_metrics: [ + { + name: "speed", + unit: "bps", + }, + ], + xpcshell_cycles: 13, + verbose: true, + try_platform: ["linux", "mac"], + }, + }, + tags: ["network", "http3", "quic"], +}; + +var performance = performance || {}; +performance.now = (function() { + return ( + performance.now || + performance.mozNow || + performance.msNow || + performance.oNow || + performance.webkitNow || + Date.now + ); +})(); + +let h3Route; +let httpsOrigin; +let h3AltSvc; + +let prefs; + +let tests = [ + // This test must be the first because it setsup alt-svc connection, that + // other tests use. + test_https_alt_svc, + test_download, + testsDone, +]; + +let current_test = 0; + +function run_next_test() { + if (current_test < tests.length) { + dump("starting test number " + current_test + "\n"); + tests[current_test](); + current_test++; + } +} + +function run_test() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + let h3Port = env.get("MOZHTTP3_PORT"); + Assert.notEqual(h3Port, null); + Assert.notEqual(h3Port, ""); + h3AltSvc = ":" + h3Port; + + h3Route = "foo.example.com:" + h3Port; + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.http3.enabled", true); + prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + // We always resolve elements of localDomains as it's hardcoded without the + // following pref: + prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); + + // The certificate for the http3server server is for foo.example.com and + // is signed by http2-ca.pem so add that cert to the trust list as a + // signing cert. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + httpsOrigin = "https://foo.example.com:" + h2Port + "/"; + + run_next_test(); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +let Http3CheckListener = function() {}; + +Http3CheckListener.prototype = { + onDataAvailableFired: false, + expectedRoute: "", + + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + Assert.equal(request.status, Cr.NS_OK); + Assert.equal(request.responseStatus, 200); + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.onDataAvailableFired = true; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + dump("status is " + status + "\n"); + // Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + Assert.equal(routed, this.expectedRoute); + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + Assert.equal(this.onDataAvailableFired, true); + }, +}; + +let WaitForHttp3Listener = function() {}; + +WaitForHttp3Listener.prototype = new Http3CheckListener(); + +WaitForHttp3Listener.prototype.uri = ""; +WaitForHttp3Listener.prototype.h3AltSvc = ""; + +WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + Assert.equal(status, Cr.NS_OK); + let routed = "NA"; + try { + routed = request.getRequestHeader("Alt-Used"); + } catch (e) {} + dump("routed is " + routed + "\n"); + + if (routed == this.expectedRoute) { + Assert.equal(routed, this.expectedRoute); // always true, but a useful log + + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + run_next_test(); + } else { + dump("poll later for alt svc mapping\n"); + do_test_pending(); + do_timeout(500, () => { + doTest(this.uri, this.expectedRoute, this.h3AltSvc); + }); + } + + do_test_finished(); +}; + +function doTest(uri, expectedRoute, altSvc) { + let chan = makeChan(uri); + let listener = new WaitForHttp3Listener(); + listener.uri = uri; + listener.expectedRoute = expectedRoute; + listener.h3AltSvc = altSvc; + chan.setRequestHeader("x-altsvc", altSvc, false); + chan.asyncOpen(listener); +} + +// Test Alt-Svc for http3. +// H2 server returns alt-svc=h3-27=:h3port +function test_https_alt_svc() { + dump("test_https_alt_svc()\n"); + + do_test_pending(); + doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc); +} + +let PerfHttp3Listener = function() {}; + +PerfHttp3Listener.prototype = new Http3CheckListener(); +PerfHttp3Listener.prototype.amount = 0; +PerfHttp3Listener.prototype.bytesRead = 0; +PerfHttp3Listener.prototype.startTime = 0; + +PerfHttp3Listener.prototype.onStartRequest = function testOnStartRequest( + request +) { + this.startTime = performance.now(); + Http3CheckListener.prototype.onStartRequest.call(this, request); +}; + +PerfHttp3Listener.prototype.onDataAvailable = function testOnStopRequest( + request, + stream, + off, + cnt +) { + this.bytesRead += cnt; + Http3CheckListener.prototype.onDataAvailable.call( + this, + request, + stream, + off, + cnt + ); +}; + +PerfHttp3Listener.prototype.onStopRequest = function testOnStopRequest( + request, + status +) { + let stopTime = performance.now(); + Http3CheckListener.prototype.onStopRequest.call(this, request, status); + if (this.bytesRead != this.amount) { + dump("Wrong number of bytes..."); + } else { + let speed = (this.bytesRead * 1000) / (stopTime - this.startTime); + info("perfMetrics", { speed }); + } + run_next_test(); + do_test_finished(); +}; + +function test_download() { + dump("test_download()\n"); + + let listener = new PerfHttp3Listener(); + listener.expectedRoute = h3Route; + listener.amount = 1024 * 1024; + let chan = makeChan(httpsOrigin + listener.amount.toString()); + chan.asyncOpen(listener); + do_test_pending(); +} + +function testsDone() { + prefs.clearUserPref("network.http.http3.enabled"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); + dump("testDone\n"); + do_test_pending(); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_http3_server_not_existing.js b/netwerk/test/unit/test_http3_server_not_existing.js new file mode 100644 index 0000000000..31ce77cc36 --- /dev/null +++ b/netwerk/test/unit/test_http3_server_not_existing.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +let httpsUri; + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("network.http.http3.enabled"); + Services.prefs.clearUserPref("network.dns.localDomains"); + Services.prefs.clearUserPref("network.dns.disableIPv6"); + Services.prefs.clearUserPref( + "network.http.http3.alt-svc-mapping-for-testing" + ); + Services.prefs.clearUserPref("network.http.http3.backup_timer_delay"); + dump("cleanup done\n"); +}); + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish() { + resolve(); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan() { + let chan = NetUtil.newChannel({ + uri: httpsUri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +function altsvcSetupPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +add_task(async function test_fatal_error() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + let h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + + Services.prefs.setBoolPref("network.http.http3.enabled", true); + Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com"); + Services.prefs.setBoolPref("network.dns.disableIPv6", true); + // Set AltSvc to point to not existing HTTP3 server on port 443 + Services.prefs.setCharPref( + "network.http.http3.alt-svc-mapping-for-testing", + "foo.example.com;h3-27=:443" + ); + Services.prefs.setIntPref("network.http.http3.backup_timer_delay", 0); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + httpsUri = "https://foo.example.com:" + h2Port + "/"; +}); + +add_task(async function test_fatal_stream_error() { + let result = 1; + // We need to loop here because we need to wait for AltSvc storage to + // to be started. + // We also do not have a way to verify that HTTP3 has been tried, because + // the fallback is automatic, so try a couple of times. + do { + // We need to close HTTP2 connections, otherwise our connection pooling + // will dispatch the request over already existing HTTP2 connection. + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); + result++; + } while (result < 5); +}); + +let CheckOnlyHttp2Listener = function() {}; + +CheckOnlyHttp2Listener.prototype = { + onStartRequest: function testOnStartRequest(request) {}, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + Assert.equal(status, Cr.NS_OK); + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h2"); + this.finish(); + }, +}; + +add_task(async function test_no_http3_after_error() { + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); + +// also after all connections are closed. +add_task(async function test_no_http3_after_error2() { + Services.obs.notifyObservers(null, "net:prune-all-connections"); + let chan = makeChan(); + let listener = new CheckOnlyHttp2Listener(); + await altsvcSetupPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_http3_trans_close.js b/netwerk/test/unit/test_http3_trans_close.js new file mode 100644 index 0000000000..a7525a5d4b --- /dev/null +++ b/netwerk/test/unit/test_http3_trans_close.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +registerCleanupFunction(async () => { + http3_clear_prefs(); +}); + +add_task(async function setup() { + await http3_setup_tests(); +}); + +let Http3Listener = function() {}; + +Http3Listener.prototype = { + expectedAmount: 0, + expectedStatus: Cr.NS_OK, + amount: 0, + + onStartRequest: function testOnStartRequest(request) { + Assert.equal(request.status, this.expectedStatus); + if (Components.isSuccessCode(this.expectedStatus)) { + Assert.equal(request.responseStatus, 200); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + this.amount += cnt; + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + let httpVersion = ""; + try { + httpVersion = request.protocolVersion; + } catch (e) {} + Assert.equal(httpVersion, "h3"); + Assert.equal(this.amount, this.expectedAmount); + + this.finish(); + }, +}; + +function chanPromise(chan, listener) { + return new Promise(resolve => { + function finish(result) { + resolve(result); + } + listener.finish = finish; + chan.asyncOpen(listener); + }); +} + +function makeChan(uri) { + let chan = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return chan; +} + +add_task(async function test_response_without_body() { + let chan = makeChan("https://foo.example.com/no_body"); + let listener = new Http3Listener(); + listener.expectedAmount = 0; + await chanPromise(chan, listener); +}); + +add_task(async function test_response_without_content_length() { + let chan = makeChan("https://foo.example.com/no_content_length"); + let listener = new Http3Listener(); + listener.expectedAmount = 4000; + await chanPromise(chan, listener); +}); + +add_task(async function test_content_length_smaller_than_data_len() { + let chan = makeChan("https://foo.example.com/content_length_smaller"); + let listener = new Http3Listener(); + // content-lentgth is 4000, but data length is 8000. + // We should return an error here - bug 1670086. + listener.expectedAmount = 4000; + await chanPromise(chan, listener); +}); diff --git a/netwerk/test/unit/test_httpResponseTimeout.js b/netwerk/test/unit/test_httpResponseTimeout.js new file mode 100644 index 0000000000..6c82758156 --- /dev/null +++ b/netwerk/test/unit/test_httpResponseTimeout.js @@ -0,0 +1,162 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var baseURL; +const kResponseTimeoutPref = "network.http.response.timeout"; +const kResponseTimeout = 1; +const kShortLivedKeepalivePref = + "network.http.tcp_keepalive.short_lived_connections"; +const kLongLivedKeepalivePref = + "network.http.tcp_keepalive.long_lived_connections"; + +const prefService = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch +); + +var server = new HttpServer(); + +function TimeoutListener(expectResponse) { + this.expectResponse = expectResponse; +} + +TimeoutListener.prototype = { + onStartRequest(request) {}, + + onDataAvailable(request, stream) {}, + + onStopRequest(request, status) { + if (this.expectResponse) { + Assert.equal(status, Cr.NS_OK); + } else { + Assert.equal(status, Cr.NS_ERROR_NET_TIMEOUT); + } + + run_next_test(); + }, +}; + +function serverStopListener() { + do_test_finished(); +} + +function testTimeout(timeoutEnabled, expectResponse) { + // Set timeout pref. + if (timeoutEnabled) { + prefService.setIntPref(kResponseTimeoutPref, kResponseTimeout); + } else { + prefService.setIntPref(kResponseTimeoutPref, 0); + } + + var chan = NetUtil.newChannel({ + uri: baseURL, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + var listener = new TimeoutListener(expectResponse); + chan.asyncOpen(listener); +} + +function testTimeoutEnabled() { + // Set a timeout value; expect a timeout and no response. + testTimeout(true, false); +} + +function testTimeoutDisabled() { + // Set a timeout value of 0; expect a response. + testTimeout(false, true); +} + +function testTimeoutDisabledByShortLivedKeepalives() { + // Enable TCP Keepalives for short lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, true); + prefService.setBoolPref(kLongLivedKeepalivePref, false); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function testTimeoutDisabledByLongLivedKeepalives() { + // Enable TCP Keepalives for long lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, false); + prefService.setBoolPref(kLongLivedKeepalivePref, true); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function testTimeoutDisabledByBothKeepalives() { + // Enable TCP Keepalives for short and long lived HTTP connections. + prefService.setBoolPref(kShortLivedKeepalivePref, true); + prefService.setBoolPref(kLongLivedKeepalivePref, true); + + // Try to set a timeout value, but expect a response without timeout. + testTimeout(true, true); +} + +function setup_tests() { + // Start tests with timeout enabled, i.e. disable TCP keepalives for HTTP. + // Reset pref in cleanup. + if (prefService.getBoolPref(kShortLivedKeepalivePref)) { + prefService.setBoolPref(kShortLivedKeepalivePref, false); + registerCleanupFunction(function() { + prefService.setBoolPref(kShortLivedKeepalivePref, true); + }); + } + if (prefService.getBoolPref(kLongLivedKeepalivePref)) { + prefService.setBoolPref(kLongLivedKeepalivePref, false); + registerCleanupFunction(function() { + prefService.setBoolPref(kLongLivedKeepalivePref, true); + }); + } + + var tests = [ + // Enable with a timeout value >0; + testTimeoutEnabled, + // Disable with a timeout value of 0; + testTimeoutDisabled, + // Disable by enabling TCP keepalive for short-lived HTTP connections. + testTimeoutDisabledByShortLivedKeepalives, + // Disable by enabling TCP keepalive for long-lived HTTP connections. + testTimeoutDisabledByLongLivedKeepalives, + // Disable by enabling TCP keepalive for both HTTP connection types. + testTimeoutDisabledByBothKeepalives, + ]; + + for (var i = 0; i < tests.length; i++) { + add_test(tests[i]); + } +} + +function setup_http_server() { + // Start server; will be stopped at test cleanup time. + server.start(-1); + baseURL = "http://localhost:" + server.identity.primaryPort + "/"; + info("Using baseURL: " + baseURL); + server.registerPathHandler("/", function(metadata, response) { + // Wait until the timeout should have passed, then respond. + response.processAsync(); + + do_timeout((kResponseTimeout + 1) * 1000 /* ms */, function() { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.write("Hello world"); + response.finish(); + }); + }); + registerCleanupFunction(function() { + server.stop(serverStopListener); + }); +} + +function run_test() { + setup_http_server(); + + setup_tests(); + + run_next_test(); +} diff --git a/netwerk/test/unit/test_http_headers.js b/netwerk/test/unit/test_http_headers.js new file mode 100644 index 0000000000..e87d4cc2bb --- /dev/null +++ b/netwerk/test/unit/test_http_headers.js @@ -0,0 +1,75 @@ +"use strict"; + +function check_request_header(chan, name, value) { + var chanValue; + try { + chanValue = chan.getRequestHeader(name); + } catch (e) { + do_throw("Expected to find header '" + name + "' but didn't find it"); + } + Assert.equal(chanValue, value); +} + +function run_test() { + var chan = NetUtil.newChannel({ + uri: "http://www.mozilla.org/", + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + check_request_header(chan, "host", "www.mozilla.org"); + check_request_header(chan, "Host", "www.mozilla.org"); + + chan.setRequestHeader("foopy", "bar", false); + check_request_header(chan, "foopy", "bar"); + + chan.setRequestHeader("foopy", "baz", true); + check_request_header(chan, "foopy", "bar, baz"); + + for (var i = 0; i < 100; ++i) { + chan.setRequestHeader("foopy" + i, i, false); + } + + for (var i = 0; i < 100; ++i) { + check_request_header(chan, "foopy" + i, i); + } + + var x = false; + try { + chan.setRequestHeader("foo:py", "baz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header with colon not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy", "b\naz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header value with newline not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy\u0080", "baz", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header name with non-ASCII not rejected"); + } + + x = false; + try { + chan.setRequestHeader("foopy", "b\u0000az", false); + } catch (e) { + x = true; + } + if (!x) { + do_throw("header value with null-byte not rejected"); + } +} diff --git a/netwerk/test/unit/test_http_sfv.js b/netwerk/test/unit/test_http_sfv.js new file mode 100644 index 0000000000..c1254865fb --- /dev/null +++ b/netwerk/test/unit/test_http_sfv.js @@ -0,0 +1,590 @@ +"use strict"; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const gService = Cc["@mozilla.org/http-sfv-service;1"].getService( + Ci.nsISFVService +); + +add_task(async function test_sfv_bare_item() { + // tests bare item + let item_int = gService.newInteger(19); + Assert.equal(item_int.value, 19, "check bare item value"); + + let item_bool = gService.newBool(true); + Assert.equal(item_bool.value, true, "check bare item value"); + item_bool.value = false; + Assert.equal(item_bool.value, false, "check bare item value"); + + let item_float = gService.newDecimal(145.45); + Assert.equal(item_float.value, 145.45); + + let item_str = gService.newString("some_string"); + Assert.equal(item_str.value, "some_string", "check bare item value"); + + let item_byte_seq = gService.newByteSequence("aGVsbG8="); + Assert.equal(item_byte_seq.value, "aGVsbG8=", "check bare item value"); + + let item_token = gService.newToken("*a"); + Assert.equal(item_token.value, "*a", "check bare item value"); +}); + +add_task(async function test_sfv_params() { + // test params + let params = gService.newParameters(); + let bool_param = gService.newBool(false); + let int_param = gService.newInteger(15); + let decimal_param = gService.newDecimal(15.45); + + params.set("bool_param", bool_param); + params.set("int_param", int_param); + params.set("decimal_param", decimal_param); + + Assert.throws( + () => { + params.get("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as key does not exist in parameters" + ); + Assert.equal( + params.get("bool_param").QueryInterface(Ci.nsISFVBool).value, + false, + "get parameter by key and check its value" + ); + Assert.equal( + params.get("int_param").QueryInterface(Ci.nsISFVInteger).value, + 15, + "get parameter by key and check its value" + ); + Assert.equal( + params.get("decimal_param").QueryInterface(Ci.nsISFVDecimal).value, + 15.45, + "get parameter by key and check its value" + ); + Assert.deepEqual( + params.keys(), + ["bool_param", "int_param", "decimal_param"], + "check that parameters contain all the expected keys" + ); + + params.delete("int_param"); + Assert.deepEqual( + params.keys(), + ["bool_param", "decimal_param"], + "check that parameter has been deleted" + ); + + Assert.throws( + () => { + params.delete("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception upon attempt to delete by non-existing key" + ); +}); + +add_task(async function test_sfv_inner_list() { + // create primitives for inner list + let item1_params = gService.newParameters(); + item1_params.set("param_1", gService.newToken("*smth")); + let item1 = gService.newItem(gService.newDecimal(172.145865), item1_params); + + let item2_params = gService.newParameters(); + item2_params.set("param_1", gService.newBool(true)); + item2_params.set("param_2", gService.newInteger(145454)); + let item2 = gService.newItem( + gService.newByteSequence("weather"), + item2_params + ); + + // create inner list + let inner_list_params = gService.newParameters(); + inner_list_params.set("inner_param", gService.newByteSequence("tests")); + let inner_list = gService.newInnerList([item1, item2], inner_list_params); + + // check inner list members & params + let inner_list_members = inner_list.QueryInterface(Ci.nsISFVInnerList).items; + let inner_list_parameters = inner_list + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + Assert.equal(inner_list_members.length, 2, "check inner list length"); + + let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value, + 172.145865, + "check inner list member value" + ); + + let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value, + "weather", + "check inner list member value" + ); + + Assert.equal( + inner_list_parameters.get("inner_param").QueryInterface(Ci.nsISFVByteSeq) + .value, + "tests", + "get inner list parameter by key and check its value" + ); +}); + +add_task(async function test_sfv_item() { + // create parameters for item + let params = gService.newParameters(); + let param1 = gService.newBool(false); + let param2 = gService.newString("str_value"); + let param3 = gService.newBool(true); + params.set("param_1", param1); + params.set("param_2", param2); + params.set("param_3", param3); + + // create item field + let item = gService.newItem(gService.newToken("*abc"), params); + + Assert.equal( + item.value.QueryInterface(Ci.nsISFVToken).value, + "*abc", + "check items's value" + ); + Assert.equal( + item.params.get("param_1").QueryInterface(Ci.nsISFVBool).value, + false, + "get item parameter by key and check its value" + ); + Assert.equal( + item.params.get("param_2").QueryInterface(Ci.nsISFVString).value, + "str_value", + "get item parameter by key and check its value" + ); + Assert.equal( + item.params.get("param_3").QueryInterface(Ci.nsISFVBool).value, + true, + "get item parameter by key and check its value" + ); + + // check item field serialization + let serialized = item.serialize(); + Assert.equal( + serialized, + `*abc;param_1=?0;param_2="str_value";param_3`, + "serialized output must match expected one" + ); +}); + +add_task(async function test_sfv_list() { + // create primitives for List + let item1_params = gService.newParameters(); + item1_params.set("param_1", gService.newToken("*smth")); + let item1 = gService.newItem(gService.newDecimal(145454.14568), item1_params); + + let item2_params = gService.newParameters(); + item2_params.set("param_1", gService.newBool(true)); + let item2 = gService.newItem( + gService.newByteSequence("weather"), + item2_params + ); + + let inner_list = gService.newInnerList( + [item1, item2], + gService.newParameters() + ); + + // create list field + let list = gService.newList([item1, inner_list]); + + // check list's members + let list_members = list.members; + Assert.equal(list_members.length, 2, "check list length"); + + // check list's member of item type + let member1 = list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + member1.value.QueryInterface(Ci.nsISFVDecimal).value, + 145454.14568, + "check list member's value" + ); + let member1_parameters = member1.params; + Assert.equal( + member1_parameters.get("param_1").QueryInterface(Ci.nsISFVToken).value, + "*smth", + "get list member's parameter by key and check its value" + ); + + // check list's member of inner list type + let inner_list_members = list_members[1].QueryInterface(Ci.nsISFVInnerList) + .items; + Assert.equal(inner_list_members.length, 2, "check inner list length"); + + let inner_item1 = inner_list_members[0].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item1.value.QueryInterface(Ci.nsISFVDecimal).value, + 145454.14568, + "check inner list member's value" + ); + + let inner_item2 = inner_list_members[1].QueryInterface(Ci.nsISFVItem); + Assert.equal( + inner_item2.value.QueryInterface(Ci.nsISFVByteSeq).value, + "weather", + "check inner list member's value" + ); + + // check inner list member's params + list_members[1] + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + + // check serialization of list field + let expected_serialized = + "145454.146;param_1=*smth, (145454.146;param_1=*smth :d2VhdGhlcg==:;param_1)"; + let actual_serialized = list.serialize(); + Assert.equal( + actual_serialized, + expected_serialized, + "serialized output must match expected one" + ); +}); + +add_task(async function test_sfv_dictionary() { + // create primitives for dictionary field + + // dict member1 + let params1 = gService.newParameters(); + params1.set("mp_1", gService.newBool(true)); + params1.set("mp_2", gService.newDecimal(68.758602)); + let member1 = gService.newItem(gService.newString("member_1"), params1); + + // dict member2 + let params2 = gService.newParameters(); + let inner_item1 = gService.newItem( + gService.newString("inner_item_1"), + gService.newParameters() + ); + let inner_item2 = gService.newItem( + gService.newToken("tok"), + gService.newParameters() + ); + let member2 = gService.newInnerList([inner_item1, inner_item2], params2); + + // dict member3 + let params_3 = gService.newParameters(); + params_3.set("mp_1", gService.newInteger(6586)); + let member3 = gService.newItem(gService.newString("member_3"), params_3); + + // create dictionary field + let dict = gService.newDictionary(); + dict.set("key_1", member1); + dict.set("key_2", member2); + dict.set("key_3", member3); + + // check dictionary keys + let expected = ["key_1", "key_2", "key_3"]; + Assert.deepEqual( + expected, + dict.keys(), + "check dictionary contains all the expected keys" + ); + + // check dictionary members + Assert.throws( + () => { + dict.get("key_4"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as key does not exist in dictionary" + ); + + // let dict_member1 = dict.get("key_1").QueryInterface(Ci.nsISFVItem); + let dict_member2 = dict.get("key_2").QueryInterface(Ci.nsISFVInnerList); + let dict_member3 = dict.get("key_3").QueryInterface(Ci.nsISFVItem); + + // Assert.equal( + // dict_member1.value.QueryInterface(Ci.nsISFVString).value, + // "member_1", + // "check dictionary member's value" + // ); + // Assert.equal( + // dict_member1.params.get("mp_1").QueryInterface(Ci.nsISFVBool).value, + // true, + // "get dictionary member's parameter by key and check its value" + // ); + // Assert.equal( + // dict_member1.params.get("mp_2").QueryInterface(Ci.nsISFVDecimal).value, + // "68.758602", + // "get dictionary member's parameter by key and check its value" + // ); + + let dict_member2_items = dict_member2.QueryInterface(Ci.nsISFVInnerList) + .items; + let dict_member2_params = dict_member2 + .QueryInterface(Ci.nsISFVInnerList) + .params.QueryInterface(Ci.nsISFVParams); + Assert.equal( + dict_member2_items[0] + .QueryInterface(Ci.nsISFVItem) + .value.QueryInterface(Ci.nsISFVString).value, + "inner_item_1", + "get dictionary member of inner list type, and check inner list member's value" + ); + Assert.equal( + dict_member2_items[1] + .QueryInterface(Ci.nsISFVItem) + .value.QueryInterface(Ci.nsISFVToken).value, + "tok", + "get dictionary member of inner list type, and check inner list member's value" + ); + Assert.throws( + () => { + dict_member2_params.get("some_param"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception as dict member's parameters are empty" + ); + + Assert.equal( + dict_member3.value.QueryInterface(Ci.nsISFVString).value, + "member_3", + "check dictionary member's value" + ); + Assert.equal( + dict_member3.params.get("mp_1").QueryInterface(Ci.nsISFVInteger).value, + 6586, + "get dictionary member's parameter by key and check its value" + ); + + // check serialization of Dictionary field + let expected_serialized = `key_1="member_1";mp_1;mp_2=68.759, key_2=("inner_item_1" tok), key_3="member_3";mp_1=6586`; + let actual_serialized = dict.serialize(); + Assert.equal( + actual_serialized, + expected_serialized, + "serialized output must match expected one" + ); + + // check deleting dict member + dict.delete("key_2"); + Assert.deepEqual( + dict.keys(), + ["key_1", "key_3"], + "check that dictionary member has been deleted" + ); + + Assert.throws( + () => { + dict.delete("some_key"); + }, + /NS_ERROR_UNEXPECTED/, + "must throw exception upon attempt to delete by non-existing key" + ); +}); + +add_task(async function test_sfv_item_parsing() { + Assert.ok(gService.parseItem(`"str"`), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35;a "), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35; a "), "input must be parsed into Item"); + Assert.ok(gService.parseItem("12.35 "), "input must be parsed into Item"); + + Assert.throws( + () => { + gService.parseItem("12.35;\ta "); + }, + /NS_ERROR_FAILURE/, + "item parsing must fail: invalid parameters delimiter" + ); + + Assert.throws( + () => { + gService.parseItem("125666.3565648855455"); + }, + /NS_ERROR_FAILURE/, + "item parsing must fail: decimal too long" + ); +}); + +add_task(async function test_sfv_list_parsing() { + Assert.ok( + gService.parseList( + "(?1;param_1=*smth :d2VhdGhlcg==:;param_1;param_2=145454);inner_param=:d2VpcmR0ZXN0cw==:" + ), + "input must be parsed into List" + ); + Assert.ok("a, (b c)", "input must be parsed into List"); + + Assert.throws(() => { + gService.parseList("?tok", "list parsing must fail"); + }, /NS_ERROR_FAILURE/); + + Assert.throws(() => { + gService.parseList( + "a, (b, c)", + "list parsing must fail: invalid delimiter within inner list" + ); + }, /NS_ERROR_FAILURE/); + + Assert.throws( + () => { + gService.parseList("a, b c"); + }, + /NS_ERROR_FAILURE/, + "list parsing must fail: invalid delimiter" + ); +}); + +add_task(async function test_sfv_dict_parsing() { + Assert.ok( + gService.parseDictionary(`abc=123;a=1;b=2, def=456, ghi=789;q=9;r="+w"`), + "input must be parsed into Dictionary" + ); + Assert.ok( + gService.parseDictionary("a=1\t,\t\t\t c=*"), + "input must be parsed into Dictionary" + ); + Assert.ok( + gService.parseDictionary("a=1\t,\tc=* \t\t"), + "input must be parsed into Dictionary" + ); + + Assert.throws( + () => { + gService.parseDictionary("a=1\t,\tc=*,"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: trailing comma" + ); + + Assert.throws( + () => { + gService.parseDictionary("a=1 c=*"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: invalid delimiter" + ); + + Assert.throws( + () => { + gService.parseDictionary("INVALID_key=1, c=*"); + }, + /NS_ERROR_FAILURE/, + "dictionary parsing must fail: invalid key format, can't be in uppercase" + ); +}); + +add_task(async function test_sfv_list_parse_serialize() { + let list_field = gService.parseList("1 , 42, (42 43)"); + Assert.equal( + list_field.serialize(), + "1, 42, (42 43)", + "serialized output must match expected one" + ); + + // create new inner list with parameters + let inner_list_params = gService.newParameters(); + inner_list_params.set("key1", gService.newString("value1")); + inner_list_params.set("key2", gService.newBool(true)); + inner_list_params.set("key3", gService.newBool(false)); + let inner_list_items = [ + gService.newItem( + gService.newDecimal(-1865.75653), + gService.newParameters() + ), + gService.newItem(gService.newToken("token"), gService.newParameters()), + gService.newItem(gService.newString(`no"yes`), gService.newParameters()), + ]; + let new_list_member = gService.newInnerList( + inner_list_items, + inner_list_params + ); + + // set one of list members to inner list and check it's serialized as expected + let members = list_field.members; + members[1] = new_list_member; + list_field.members = members; + Assert.equal( + list_field.serialize(), + `1, (-1865.757 token "no\\"yes");key1="value1";key2;key3=?0, (42 43)`, + "update list member and check list is serialized as expected" + ); +}); + +add_task(async function test_sfv_dict_parse_serialize() { + let dict_field = gService.parseDictionary( + "a=1, b; foo=*, \tc=3, \t \tabc=123;a=1;b=2\t" + ); + Assert.equal( + dict_field.serialize(), + "a=1, b;foo=*, c=3, abc=123;a=1;b=2", + "serialized output must match expected one" + ); + + // set new value for existing dict's key + dict_field.set( + "a", + gService.newItem(gService.newInteger(165), gService.newParameters()) + ); + + // add new member to dict + dict_field.set( + "key", + gService.newItem(gService.newDecimal(45.0), gService.newParameters()) + ); + + // check dict is serialized properly after the above changes + Assert.equal( + dict_field.serialize(), + "a=165, b;foo=*, c=3, abc=123;a=1;b=2, key=45.0", + "update dictionary members and dictionary list is serialized as expected" + ); +}); + +add_task(async function test_sfv_list_parse_more() { + // check parsing of multiline header of List type + let list_field = gService.parseList("(12 abc), 12.456\t\t "); + list_field.parseMore("11, 15, tok"); + Assert.equal( + list_field.serialize(), + "(12 abc), 12.456, 11, 15, tok", + "multi-line field value parsed and serialized successfully" + ); + + // should fail parsing one more line + Assert.throws( + () => { + list_field.parseMore("(tk\t1)"); + }, + /NS_ERROR_FAILURE/, + "line parsing must fail: invalid delimiter in inner list" + ); + Assert.equal( + list_field.serialize(), + "(12 abc), 12.456, 11, 15, tok", + "parsed value must not change if parsing one more line of header fails" + ); +}); + +add_task(async function test_sfv_dict_parse_more() { + // check parsing of multiline header of Dictionary type + let dict_field = gService.parseDictionary(""); + dict_field.parseMore("key2=?0, key3=?1, key4=itm"); + dict_field.parseMore("key1, key5=11, key4=45"); + + Assert.equal( + dict_field.serialize(), + "key2=?0, key3, key4=45, key1, key5=11", + "multi-line field value parsed and serialized successfully" + ); + + // should fail parsing one more line + Assert.throws( + () => { + dict_field.parseMore("c=12, _k=13"); + }, + /NS_ERROR_FAILURE/, + "line parsing must fail: invalid key format" + ); +}); diff --git a/netwerk/test/unit/test_httpauth.js b/netwerk/test/unit/test_httpauth.js new file mode 100644 index 0000000000..9c9de82618 --- /dev/null +++ b/netwerk/test/unit/test_httpauth.js @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure the HTTP authenticated sessions are correctly cleared +// when entering and leaving the private browsing mode. + +"use strict"; + +function run_test() { + var am = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + + const kHost1 = "pbtest3.example.com"; + const kHost2 = "pbtest4.example.com"; + const kPort = 80; + const kHTTP = "http"; + const kBasic = "basic"; + const kRealm = "realm"; + const kDomain = "example.com"; + const kUser = "user"; + const kUser2 = "user2"; + const kPassword = "pass"; + const kPassword2 = "pass2"; + const kEmpty = ""; + + const PRIVATE = true; + const NOT_PRIVATE = false; + + try { + var domain = { value: kEmpty }, + user = { value: kEmpty }, + pass = { value: kEmpty }; + // simulate a login via HTTP auth outside of the private mode + am.setAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + kDomain, + kUser, + kPassword + ); + // make sure the recently added auth entry is available outside the private browsing mode + am.getAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + Assert.equal(domain.value, kDomain); + Assert.equal(user.value, kUser); + Assert.equal(pass.value, kPassword); + + // make sure the added auth entry is no longer accessible in private + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + try { + // should throw + am.getAuthIdentity( + kHTTP, + kHost1, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + do_throw( + "Auth entry should not be retrievable after entering the private browsing mode" + ); + } catch (e) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + + // simulate a login via HTTP auth inside of the private mode + am.setAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + kDomain, + kUser2, + kPassword2, + PRIVATE + ); + // make sure the recently added auth entry is available inside the private browsing mode + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + Assert.equal(domain.value, kDomain); + Assert.equal(user.value, kUser2); + Assert.equal(pass.value, kPassword2); + + try { + // make sure the recently added auth entry is not available outside the private browsing mode + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + do_throw( + "Auth entry should not be retrievable outside of private browsing mode" + ); + } catch (x) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + + // simulate leaving private browsing mode + Services.obs.notifyObservers(null, "last-pb-context-exited"); + + // make sure the added auth entry is no longer accessible in any privacy state + domain = { value: kEmpty }; + user = { value: kEmpty }; + pass = { value: kEmpty }; + try { + // should throw (not available in public mode) + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + NOT_PRIVATE + ); + do_throw( + "Auth entry should not be retrievable after exiting the private browsing mode" + ); + } catch (e) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + try { + // should throw (no longer available in private mode) + am.getAuthIdentity( + kHTTP, + kHost2, + kPort, + kBasic, + kRealm, + kEmpty, + domain, + user, + pass, + PRIVATE + ); + do_throw( + "Auth entry should not be retrievable in private mode after exiting the private browsing mode" + ); + } catch (x) { + Assert.equal(domain.value, kEmpty); + Assert.equal(user.value, kEmpty); + Assert.equal(pass.value, kEmpty); + } + } catch (e) { + do_throw("Unexpected exception while testing HTTP auth manager: " + e); + } +} diff --git a/netwerk/test/unit/test_httpcancel.js b/netwerk/test/unit/test_httpcancel.js new file mode 100644 index 0000000000..85b4f99d89 --- /dev/null +++ b/netwerk/test/unit/test_httpcancel.js @@ -0,0 +1,260 @@ +// This file ensures that canceling a channel early does not +// send the request to the server (bug 350790) +// +// I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as +// expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT: +// +// This test also checks that cancelling a channel before asyncOpen, after +// onStopRequest, or during onDataAvailable works as expected. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function inChildProcess() { + return ( + Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime) + .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); +var ReferrerInfo = Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" +); +var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + subject = subject.QueryInterface(Ci.nsIRequest); + subject.cancel(Cr.NS_BINDING_ABORTED); + + // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work + try { + subject.QueryInterface(Ci.nsIHttpChannel); + let currentReferrer = subject.getRequestHeader("Referer"); + Assert.equal(currentReferrer, "http://site1.com/"); + var uri = ios.newURI("http://site2.com"); + subject.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + uri + ); + } catch (ex) { + do_throw("Exception: " + ex); + } + }, +}; + +let cancelDuringOnStartListener = { + onStartRequest: function test_onStartR(request) { + Assert.equal(request.status, Cr.NS_BINDING_ABORTED); + + // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail + try { + request.QueryInterface(Ci.nsIHttpChannel); + let currentReferrer = request.getRequestHeader("Referer"); + Assert.equal(currentReferrer, "http://site2.com/"); + var uri = ios.newURI("http://site3.com/"); + + // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + env.set("NECKO_ERRORS_ARE_FATAL", "0"); + // we expect setting referrer to fail + try { + request.referrerInfo = new ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + uri + ); + do_throw("Error should have been thrown before getting here"); + } catch (ex) {} + } catch (ex) { + do_throw("Exception: " + ex); + } + }, + + onDataAvailable: function test_ODA() { + do_throw("Should not get any data!"); + }, + + onStopRequest: function test_onStopR(request, status) { + this.resolved(); + }, +}; + +var cancelDuringOnDataListener = { + data: "", + channel: null, + receivedSomeData: null, + onStartRequest: function test_onStartR(request, ctx) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + Assert.ok(!string.includes("b")); + this.data += string; + this.channel.cancel(Cr.NS_BINDING_ABORTED); + if (this.receivedSomeData) { + this.receivedSomeData(); + } + }, + + onStopRequest: function test_onStopR(request, ctx, status) { + Assert.ok(this.data.includes("a"), `data: ${this.data}`); + Assert.equal(request.status, Cr.NS_BINDING_ABORTED); + this.resolved(); + }, +}; + +function makeChan(url) { + var chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + + // ENSURE_CALLED_BEFORE_CONNECT: set original value + var uri = ios.newURI("http://site1.com"); + chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); + return chan; +} + +var httpserv = null; + +add_task(async function setup() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/failtest", failtest); + httpserv.registerPathHandler("/cancel_middle", cancel_middle); + httpserv.registerPathHandler("/normal_response", normal_response); + httpserv.start(-1); + + registerCleanupFunction(async () => { + await new Promise(resolve => httpserv.stop(resolve)); + }); +}); + +add_task(async function test_cancel_during_onModifyRequest() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/failtest" + ); + + if (!inChildProcess()) { + Services.obs.addObserver(observer, "http-on-modify-request"); + } else { + do_send_remote_message("register-observer"); + await do_await_remote_message("register-observer-done"); + } + + await new Promise(resolve => { + cancelDuringOnStartListener.resolved = resolve; + chan.asyncOpen(cancelDuringOnStartListener); + }); + + if (!inChildProcess()) { + Services.obs.removeObserver(observer, "http-on-modify-request"); + } else { + do_send_remote_message("unregister-observer"); + await do_await_remote_message("unregister-observer-done"); + } +}); + +add_task(async function test_cancel_before_asyncOpen() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/failtest" + ); + + chan.cancel(Cr.NS_BINDING_ABORTED); + + Assert.throws( + () => { + chan.asyncOpen(cancelDuringOnStartListener); + }, + /NS_BINDING_ABORTED/, + "cannot open if already cancelled" + ); +}); + +add_task(async function test_cancel_during_onData() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle" + ); + + await new Promise(resolve => { + cancelDuringOnDataListener.resolved = resolve; + cancelDuringOnDataListener.channel = chan; + chan.asyncOpen(cancelDuringOnDataListener); + }); +}); + +var cancelAfterOnStopListener = { + data: "", + channel: null, + onStartRequest: function test_onStartR(request, ctx) { + Assert.equal(request.status, Cr.NS_OK); + }, + + onDataAvailable: function test_ODA(request, stream, offset, count) { + let string = NetUtil.readInputStreamToString(stream, count); + this.data += string; + }, + + onStopRequest: function test_onStopR(request, status) { + info("onStopRequest"); + Assert.equal(request.status, Cr.NS_OK); + this.resolved(); + }, +}; + +add_task(async function test_cancel_after_onStop() { + var chan = makeChan( + "http://localhost:" + httpserv.identity.primaryPort + "/normal_response" + ); + + await new Promise(resolve => { + cancelAfterOnStopListener.resolved = resolve; + cancelAfterOnStopListener.channel = chan; + chan.asyncOpen(cancelAfterOnStopListener); + }); + Assert.equal(chan.status, Cr.NS_OK); + + // For now it's unclear if cancelling after onStop should throw, + // silently fail, or overwrite the channel's status as we currently do. + // See discussion in bug 1553083 + chan.cancel(Cr.NS_BINDING_ABORTED); + Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); +}); + +// PATHS + +// /failtest +function failtest(metadata, response) { + do_throw("This should not be reached"); +} + +function cancel_middle(metadata, response) { + response.processAsync(); + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let str1 = "a".repeat(128 * 1024); + response.write(str1, str1.length); + response.bodyOutputStream.flush(); + + let p = new Promise(resolve => { + cancelDuringOnDataListener.receivedSomeData = resolve; + }); + p.then(() => { + let str1 = "b".repeat(128 * 1024); + response.write(str1, str1.length); + response.finish(); + }); +} + +function normal_response(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + let str1 = "Is this normal?"; + response.write(str1, str1.length); +} diff --git a/netwerk/test/unit/test_httpssvc_https_upgrade.js b/netwerk/test/unit/test_httpssvc_https_upgrade.js new file mode 100644 index 0000000000..789ee0e7a6 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_https_upgrade.js @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; +let listen; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + Services.prefs.setCharPref( + "network.trr.uri", + "https://foo.example.com:" + h2Port + "/httpssvc_as_altsvc" + ); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +// When observer is specified, the channel will be suspended when receiving +// "http-on-modify-request". +function channelOpenPromise(chan, flags, observer) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + if (observer) { + let topic = "http-on-modify-request"; + Services.obs.addObserver(observer, topic); + } + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +class EventSinkListener { + getInterface(iid) { + if (iid.equals(Ci.nsIChannelEventSink)) { + return this; + } + } + asyncOnChannelRedirect(oldChan, newChan, flags, callback) { + Assert.equal(oldChan.URI.hostPort, newChan.URI.hostPort); + Assert.equal(oldChan.URI.scheme, "http"); + Assert.equal(newChan.URI.scheme, "https"); + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +} + +EventSinkListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIChannelEventSink", +]); + +// Test if the request is upgraded to https with a HTTPSSVC record. +add_task(async function testUseHTTPSSVCAsHSTS() { + dns.clearCache(true); + + let dnsListener = new DNSListener(); + + // Do DNS resolution before creating the channel, so the HTTPSSVC record will + // be resolved from the cache. + let request = dns.asyncResolve( + "test.httpssvc.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + dnsListener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, , inStatus] = await dnsListener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + // Since the HTTPS RR should be served from cache, the DNS record is available + // before nsHttpChannel::MaybeUseHTTPSRRForUpgrade() is called. + let chan = makeChan(`http://test.httpssvc.com:80/server-timing`); + let listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + let [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + chan = makeChan(`http://test.httpssvc.com:80/server-timing`); + listener = new EventSinkListener(); + chan.notificationCallbacks = listener; + + [req] = await channelOpenPromise(chan); + + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); +}); + +// Test the case that we got an invalid DNS response. In this case, +// nsHttpChannel::OnHTTPSRRAvailable is called after +// nsHttpChannel::MaybeUseHTTPSRRForUpgrade. +add_task(async function testInvalidDNSResult() { + dns.clearCache(true); + + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.notexisted.com", + httpserv.identity.primaryPort + ); + + let chan = makeChan( + `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` + ); + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +// The same test as above, but nsHttpChannel::MaybeUseHTTPSRRForUpgrade is +// called after nsHttpChannel::OnHTTPSRRAvailable. +add_task(async function testInvalidDNSResult1() { + dns.clearCache(true); + + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + httpserv.identity.setPrimary( + "http", + "foo.notexisted.com", + httpserv.identity.primaryPort + ); + + let chan = makeChan( + `http://foo.notexisted.com:${httpserv.identity.primaryPort}/` + ); + + let topic = "http-on-modify-request"; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if (aTopic == topic) { + Services.obs.removeObserver(observer, topic); + let channel = aSubject.QueryInterface(Ci.nsIChannel); + channel.suspend(); + let dnsListener = { + QueryInterface: ChromeUtils.generateQI(["nsIDNSListener"]), + onLookupComplete(inRequest, inRecord, inStatus) { + channel.resume(); + }, + }; + dns.asyncResolve( + "foo.notexisted.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + dnsListener, + mainThread, + defaultOriginAttributes + ); + } + }, + }; + + let [, response] = await channelOpenPromise(chan, 0, observer); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); + +add_task(async function testLiteralIP() { + let httpserv = new HttpServer(); + let content = "ok"; + httpserv.registerPathHandler("/", function handler(metadata, response) { + response.setHeader("Content-Length", `${content.length}`); + response.bodyOutputStream.write(content, content.length); + }); + httpserv.start(-1); + + let chan = makeChan(`http://127.0.0.1:${httpserv.identity.primaryPort}/`); + let [, response] = await channelOpenPromise(chan); + Assert.equal(response, content); + await new Promise(resolve => httpserv.stop(resolve)); +}); diff --git a/netwerk/test/unit/test_httpssvc_iphint.js b/netwerk/test/unit/test_httpssvc_iphint.js new file mode 100644 index 0000000000..f54fa9f457 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_iphint.js @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; +let listen; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +// Test if IP hint addresses can be accessed as regular A/AAAA records. +add_task(async function testStoreIPHint() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.IPHint.com", "HTTPS", [ + { + name: "test.IPHint.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.IPHint.com", + values: [ + { key: "alpn", value: ["h2", "h3"] }, + { key: "port", value: 8888 }, + { key: "ipv4hint", value: ["1.2.3.4", "5.6.7.8"] }, + { key: "ipv6hint", value: ["::1", "fe80::794f:6d2c:3d5e:7836"] }, + ], + }, + }, + ]); + + let listener = new DNSListener(); + + let request = dns.asyncResolve( + "test.IPHint.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "test.IPHint.com"); + Assert.equal(answer[0].values.length, 4); + Assert.deepEqual( + answer[0].values[0].QueryInterface(Ci.nsISVCParamAlpn).alpn, + ["h2", "h3"], + "got correct answer" + ); + Assert.equal( + answer[0].values[1].QueryInterface(Ci.nsISVCParamPort).port, + 8888, + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[0] + .address, + "1.2.3.4", + "got correct answer" + ); + Assert.equal( + answer[0].values[2].QueryInterface(Ci.nsISVCParamIPv4Hint).ipv4Hint[1] + .address, + "5.6.7.8", + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[0] + .address, + "::1", + "got correct answer" + ); + Assert.equal( + answer[0].values[3].QueryInterface(Ci.nsISVCParamIPv6Hint).ipv6Hint[1] + .address, + "fe80::794f:6d2c:3d5e:7836", + "got correct answer" + ); + + async function verifyAnswer(flags, answer) { + let listener = new DNSListener(); + let request = dns.asyncResolve( + "test.IPHint.com", + dns.RESOLVE_TYPE_DEFAULT, + flags, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + inRecord.QueryInterface(Ci.nsIDNSAddrRecord); + let addresses = []; + while (inRecord.hasMore()) { + addresses.push(inRecord.getNextAddrAsString()); + } + Assert.deepEqual(addresses, answer); + } + + await verifyAnswer(Ci.nsIDNSService.RESOLVE_IP_HINT, [ + "1.2.3.4", + "5.6.7.8", + "::1", + "fe80::794f:6d2c:3d5e:7836", + ]); + + await verifyAnswer( + Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV4, + ["::1", "fe80::794f:6d2c:3d5e:7836"] + ); + + await verifyAnswer( + Ci.nsIDNSService.RESOLVE_IP_HINT | Ci.nsIDNSService.RESOLVE_DISABLE_IPV6, + ["1.2.3.4", "5.6.7.8"] + ); +}); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan) { + return new Promise(resolve => { + function finish(req, buffer) { + resolve([req, buffer]); + } + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, CL_ALLOW_UNKNOWN_CL)); + }); +} + +// Test if we can connect to the server with the IP hint address. +add_task(async function testConnectionWithIPHint() { + dns.clearCache(true); + prefs.setIntPref("network.trr.mode", 3); + prefs.setCharPref( + "network.trr.uri", + "https://127.0.0.1:" + h2Port + "/httpssvc_use_iphint" + ); + + // Resolving test.iphint.com should be failed. + let listener = new DNSListener(); + let request = dns.asyncResolve( + "test.iphint.com", + dns.RESOLVE_TYPE_DEFAULT, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, , inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal( + inStatus, + Cr.NS_ERROR_UNKNOWN_HOST, + "status is NS_ERROR_UNKNOWN_HOST" + ); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + // The connection should be succeeded since the IP hint is 127.0.0.1. + let chan = makeChan(`http://test.iphint.com:8080/`); + // Note that the partitionKey stored in DNS cache would be + // "%28https%2Ciphint.com%29". The http request to test.iphint.com will be + // upgraded to https and the ip hint address will be used by the https + // request in the end. + let [req] = await channelOpenPromise(chan); + req.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(req.getResponseHeader("x-connection-http2"), "yes"); + + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); +}); diff --git a/netwerk/test/unit/test_httpssvc_priority.js b/netwerk/test/unit/test_httpssvc_priority.js new file mode 100644 index 0000000000..140adb9ab8 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_priority.js @@ -0,0 +1,235 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); +} + +setup(); +registerCleanupFunction(() => { + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.echconfig.enabled"); +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +add_task(async function testPriorityAndECHConfig() { + let trrServer = new TRRServer(); + registerCleanupFunction(async () => { + await trrServer.stop(); + }); + await trrServer.start(); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", false); + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + await trrServer.registerDoHAnswers("test.priority.com", "HTTPS", [ + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "test.p1.com", + values: [{ key: "alpn", value: ["h2", "h3"] }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 4, + name: "test.p4.com", + values: [{ key: "echconfig", value: "456..." }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 3, + name: "test.p3.com", + values: [{ key: "ipv4hint", value: "1.2.3.4" }], + }, + }, + { + name: "test.priority.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 2, + name: "test.p2.com", + values: [{ key: "echconfig", value: "123..." }], + }, + }, + ]); + + let listener = new DNSListener(); + + let request = dns.asyncResolve( + "test.priority.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, inRecord, inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + let answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer.length, 4); + + Assert.equal(answer[0].priority, 1); + Assert.equal(answer[0].name, "test.p1.com"); + + Assert.equal(answer[1].priority, 2); + Assert.equal(answer[1].name, "test.p2.com"); + + Assert.equal(answer[2].priority, 3); + Assert.equal(answer[2].name, "test.p3.com"); + + Assert.equal(answer[3].priority, 4); + Assert.equal(answer[3].name, "test.p4.com"); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + dns.clearCache(true); + listener = new DNSListener(); + + request = dns.asyncResolve( + "test.priority.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + [inRequest, inRecord, inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + answer = inRecord.QueryInterface(Ci.nsIDNSHTTPSSVCRecord).records; + Assert.equal(answer.length, 4); + + Assert.equal(answer[0].priority, 2); + Assert.equal(answer[0].name, "test.p2.com"); + Assert.equal( + answer[0].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "123...", + "got correct answer" + ); + + Assert.equal(answer[1].priority, 4); + Assert.equal(answer[1].name, "test.p4.com"); + Assert.equal( + answer[1].values[0].QueryInterface(Ci.nsISVCParamEchConfig).echconfig, + "456...", + "got correct answer" + ); + + Assert.equal(answer[2].priority, 1); + Assert.equal(answer[2].name, "test.p1.com"); + + Assert.equal(answer[3].priority, 3); + Assert.equal(answer[3].name, "test.p3.com"); +}); diff --git a/netwerk/test/unit/test_httpssvc_retry_with_ech.js b/netwerk/test/unit/test_httpssvc_retry_with_ech.js new file mode 100644 index 0000000000..b0fbc26867 --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_retry_with_ech.js @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; +let listen; +let trrServer; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.security.esni.enabled", false); + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + add_tls_server_setup( + "EncryptedClientHelloServer", + "../../../security/manager/ssl/tests/unit/test_encrypted_client_hello" + ); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.security.esni.enabled"); + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + prefs.clearUserPref("network.dns.echconfig.enabled"); + prefs.clearUserPref("network.dns.echconfig.fallback_to_origin"); + if (trrServer) { + await trrServer.stop(); + } +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function testConnectWithECH() { + const ECH_CONFIG_FIXED = + "AEr+CABGABZlY2gtcHVibGljLmV4YW1wbGUuY29tACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAgAAQAAQADADIAAA=="; + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", [ + { + name: "ech-private.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "ech-private.example.com", + values: [ + { key: "alpn", value: "http/1.1" }, + { key: "port", value: 8443 }, + { + key: "echconfig", + value: ECH_CONFIG_FIXED, + needBase64Decode: true, + }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("ech-private.example.com", "A", [ + { + name: "ech-private.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let listener = new DNSListener(); + + let request = dns.asyncResolve( + "ech-private.example.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, , inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + let chan = makeChan(`https://ech-private.example.com`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + let securityInfo = chan.securityInfo.QueryInterface( + Ci.nsITransportSecurityInfo + ); + Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH"); + + await trrServer.stop(); +}); + +add_task(async function testEchRetry() { + dns.clearCache(true); + + const ECH_CONFIG_TRUSTED_RETRY = + "AEr+CABGABZlY2gtcHVibGljLmV4YW1wbGUuY29tACCKB1Y5SfrGIyk27W82xPpzWTDs3q72c04xSurDWlb9CgAgAAQAAQABADIAAA=="; + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers("ech-private.example.com", "HTTPS", [ + { + name: "ech-private.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "ech-private.example.com", + values: [ + { key: "alpn", value: "http/1.1" }, + { key: "port", value: 8443 }, + { + key: "echconfig", + value: ECH_CONFIG_TRUSTED_RETRY, + needBase64Decode: true, + }, + ], + }, + }, + ]); + + await trrServer.registerDoHAnswers("ech-private.example.com", "A", [ + { + name: "ech-private.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let listener = new DNSListener(); + + let request = dns.asyncResolve( + "ech-private.example.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, , inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + let chan = makeChan(`https://ech-private.example.com`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + let securityInfo = chan.securityInfo.QueryInterface( + Ci.nsITransportSecurityInfo + ); + Assert.ok(securityInfo.isAcceptedEch, "This host should have accepted ECH"); + + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_httpssvc_retry_without_ech.js b/netwerk/test/unit/test_httpssvc_retry_without_ech.js new file mode 100644 index 0000000000..b149fcbdcf --- /dev/null +++ b/netwerk/test/unit/test_httpssvc_retry_without_ech.js @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); + +let prefs; +let h2Port; +let listen; +let trrServer; + +const dns = Cc["@mozilla.org/network/dns-service;1"].getService( + Ci.nsIDNSService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); +const threadManager = Cc["@mozilla.org/thread-manager;1"].getService( + Ci.nsIThreadManager +); +const mainThread = threadManager.currentThread; + +const defaultOriginAttributes = {}; + +function setup() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + prefs.setBoolPref("network.security.esni.enabled", false); + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + // the TRR server is on 127.0.0.1 + prefs.setCharPref("network.trr.bootstrapAddress", "127.0.0.1"); + + // make all native resolve calls "secretly" resolve localhost instead + prefs.setBoolPref("network.dns.native-is-localhost", true); + + // 0 - off, 1 - race, 2 TRR first, 3 TRR only, 4 shadow + prefs.setIntPref("network.trr.mode", 2); // TRR first + prefs.setBoolPref("network.trr.wait-for-portal", false); + // don't confirm that TRR is working, just go! + prefs.setCharPref("network.trr.confirmationNS", "skip"); + + // So we can change the pref without clearing the cache to check a pushed + // record with a TRR path that fails. + Services.prefs.setBoolPref("network.trr.clear-cache-on-pref-change", false); + + Services.prefs.setBoolPref("network.dns.upgrade_with_https_rr", true); + Services.prefs.setBoolPref("network.dns.use_https_rr_as_altsvc", true); + Services.prefs.setBoolPref("network.dns.echconfig.enabled", true); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + const certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + // An arbitrary, non-ECH server. + add_tls_server_setup( + "DelegatedCredentialsServer", + "../../../security/manager/ssl/tests/unit/test_delegated_credentials" + ); + + let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent); + nssComponent.clearSSLExternalAndInternalSessionCache(); +} + +setup(); +registerCleanupFunction(async () => { + prefs.clearUserPref("network.security.esni.enabled"); + prefs.clearUserPref("network.http.spdy.enabled"); + prefs.clearUserPref("network.http.spdy.enabled.http2"); + prefs.clearUserPref("network.dns.localDomains"); + prefs.clearUserPref("network.dns.native-is-localhost"); + prefs.clearUserPref("network.trr.mode"); + prefs.clearUserPref("network.trr.uri"); + prefs.clearUserPref("network.trr.credentials"); + prefs.clearUserPref("network.trr.wait-for-portal"); + prefs.clearUserPref("network.trr.allow-rfc1918"); + prefs.clearUserPref("network.trr.useGET"); + prefs.clearUserPref("network.trr.confirmationNS"); + prefs.clearUserPref("network.trr.bootstrapAddress"); + prefs.clearUserPref("network.trr.request-timeout"); + prefs.clearUserPref("network.trr.clear-cache-on-pref-change"); + prefs.clearUserPref("network.dns.upgrade_with_https_rr"); + prefs.clearUserPref("network.dns.use_https_rr_as_altsvc"); + prefs.clearUserPref("network.dns.echconfig.enabled"); + prefs.clearUserPref("network.dns.echconfig.fallback_to_origin"); + if (trrServer) { + await trrServer.stop(); + } +}); + +class DNSListener { + constructor() { + this.promise = new Promise(resolve => { + this.resolve = resolve; + }); + } + onLookupComplete(inRequest, inRecord, inStatus) { + this.resolve([inRequest, inRecord, inStatus]); + } + // So we can await this as a promise. + then() { + return this.promise.then.apply(this.promise, arguments); + } +} + +DNSListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIDNSListener", +]); + +function makeChan(url) { + let chan = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); + return chan; +} + +function channelOpenPromise(chan, flags) { + return new Promise(resolve => { + function finish(req, buffer) { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + resolve([req, buffer]); + } + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + let internal = chan.QueryInterface(Ci.nsIHttpChannelInternal); + internal.setWaitForHTTPSSVCRecord(); + chan.asyncOpen(new ChannelListener(finish, null, flags)); + }); +} + +add_task(async function testRetryWithoutECH() { + const ECH_CONFIG_FIXED = + "AFH+CABNAB1kZWxlZ2F0ZWQtZW5hYmxlZC5leGFtcGxlLmNvbQAgigdWOUn6xiMpNu1vNsT6c1kw7N6u9nNOMUrqw1pW/QoAIAAEAAEAAwAyAAA="; + trrServer = new TRRServer(); + await trrServer.start(); + + Services.prefs.setIntPref("network.trr.mode", 3); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port}/dns-query` + ); + + // Only the last record is valid to use. + await trrServer.registerDoHAnswers( + "delegated-disabled.example.com", + "HTTPS", + [ + { + name: "delegated-disabled.example.com", + ttl: 55, + type: "HTTPS", + flush: false, + data: { + priority: 1, + name: "delegated-disabled.example.com", + values: [ + { + key: "echconfig", + value: ECH_CONFIG_FIXED, + needBase64Decode: true, + }, + ], + }, + }, + ] + ); + + await trrServer.registerDoHAnswers("delegated-disabled.example.com", "A", [ + { + name: "delegated-disabled.example.com", + ttl: 55, + type: "A", + flush: false, + data: "127.0.0.1", + }, + ]); + + let listener = new DNSListener(); + + let request = dns.asyncResolve( + "delegated-disabled.example.com", + dns.RESOLVE_TYPE_HTTPSSVC, + 0, + null, // resolverInfo + listener, + mainThread, + defaultOriginAttributes + ); + + let [inRequest, , inStatus] = await listener; + Assert.equal(inRequest, request, "correct request was used"); + Assert.equal(inStatus, Cr.NS_OK, "status OK"); + + let chan = makeChan(`https://delegated-disabled.example.com:8443`); + await channelOpenPromise(chan, CL_ALLOW_UNKNOWN_CL); + let securityInfo = chan.securityInfo.QueryInterface( + Ci.nsITransportSecurityInfo + ); + + Assert.ok( + !securityInfo.isAcceptedEch, + "This host should not have accepted ECH" + ); + await trrServer.stop(); +}); diff --git a/netwerk/test/unit/test_httpsuspend.js b/netwerk/test/unit/test_httpsuspend.js new file mode 100644 index 0000000000..8675b3e6bd --- /dev/null +++ b/netwerk/test/unit/test_httpsuspend.js @@ -0,0 +1,84 @@ +// This file ensures that suspending a channel directly after opening it +// suspends future notifications correctly. + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyGetter(this, "URL", function() { + return "http://localhost:" + httpserv.identity.primaryPort; +}); + +const MIN_TIME_DIFFERENCE = 3000; +const RESUME_DELAY = 5000; + +var listener = { + _lastEvent: 0, + _gotData: false, + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest(request) { + this._lastEvent = Date.now(); + request.QueryInterface(Ci.nsIRequest); + + // Insert a delay between this and the next callback to ensure message buffering + // works correctly + request.suspend(); + request.suspend(); + do_timeout(RESUME_DELAY, function() { + request.resume(); + }); + do_timeout(RESUME_DELAY + 1000, function() { + request.resume(); + }); + }, + + onDataAvailable(request, stream, offset, count) { + Assert.ok(Date.now() - this._lastEvent >= MIN_TIME_DIFFERENCE); + read_stream(stream, count); + + // Ensure that suspending and resuming inside a callback works correctly + request.suspend(); + request.suspend(); + request.resume(); + request.resume(); + + this._gotData = true; + }, + + onStopRequest(request, status) { + Assert.ok(this._gotData); + httpserv.stop(do_test_finished); + }, +}; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var httpserv = null; + +function run_test() { + httpserv = new HttpServer(); + httpserv.registerPathHandler("/woo", data); + httpserv.start(-1); + + var chan = makeChan(URL + "/woo"); + chan.QueryInterface(Ci.nsIRequest); + chan.asyncOpen(listener); + + do_test_pending(); +} + +function data(metadata, response) { + let httpbody = "0123456789"; + response.setHeader("Content-Type", "text/plain", false); + response.bodyOutputStream.write(httpbody, httpbody.length); +} diff --git a/netwerk/test/unit/test_idn_blacklist.js b/netwerk/test/unit/test_idn_blacklist.js new file mode 100644 index 0000000000..d0c77d87a1 --- /dev/null +++ b/netwerk/test/unit/test_idn_blacklist.js @@ -0,0 +1,173 @@ +// Test that URLs containing characters in the IDN blacklist are +// always displayed as punycode + +"use strict"; + +const testcases = [ + // Original Punycode or + // normalized form + // + ["\u00BC", "xn--14-c6t"], + ["\u00BD", "xn--12-c6t"], + ["\u00BE", "xn--34-c6t"], + ["\u01C3", "xn--ija"], + ["\u02D0", "xn--6qa"], + ["\u0337", "xn--4ta"], + ["\u0338", "xn--5ta"], + ["\u0589", "xn--3bb"], + ["\u05C3", "xn--rdb"], + ["\u05F4", "xn--5eb"], + ["\u0609", "xn--rfb"], + ["\u060A", "xn--sfb"], + ["\u066A", "xn--jib"], + ["\u06D4", "xn--klb"], + ["\u0701", "xn--umb"], + ["\u0702", "xn--vmb"], + ["\u0703", "xn--wmb"], + ["\u0704", "xn--xmb"], + ["\u115F", "xn--osd"], + ["\u1160", "xn--psd"], + ["\u1735", "xn--d0e"], + ["\u2027", "xn--svg"], + ["\u2028", "xn--tvg"], + ["\u2029", "xn--uvg"], + ["\u2039", "xn--bwg"], + ["\u203A", "xn--cwg"], + ["\u2041", "xn--jwg"], + ["\u2044", "xn--mwg"], + ["\u2052", "xn--0wg"], + ["\u2153", "xn--13-c6t"], + ["\u2154", "xn--23-c6t"], + ["\u2155", "xn--15-c6t"], + ["\u2156", "xn--25-c6t"], + ["\u2157", "xn--35-c6t"], + ["\u2158", "xn--45-c6t"], + ["\u2159", "xn--16-c6t"], + ["\u215A", "xn--56-c6t"], + ["\u215B", "xn--18-c6t"], + ["\u215C", "xn--38-c6t"], + ["\u215D", "xn--58-c6t"], + ["\u215E", "xn--78-c6t"], + ["\u215F", "xn--1-zjn"], + ["\u2215", "xn--w9g"], + ["\u2236", "xn--ubh"], + ["\u23AE", "xn--lmh"], + ["\u2571", "xn--hzh"], + ["\u29F6", "xn--jxi"], + ["\u29F8", "xn--lxi"], + ["\u2AFB", "xn--z4i"], + ["\u2AFD", "xn--14i"], + ["\u2FF0", "xn--85j"], + ["\u2FF1", "xn--95j"], + ["\u2FF2", "xn--b6j"], + ["\u2FF3", "xn--c6j"], + ["\u2FF4", "xn--d6j"], + ["\u2FF5", "xn--e6j"], + ["\u2FF6", "xn--f6j"], + ["\u2FF7", "xn--g6j"], + ["\u2FF8", "xn--h6j"], + ["\u2FF9", "xn--i6j"], + ["\u2FFA", "xn--j6j"], + ["\u2FFB", "xn--k6j"], + ["\u3014", "xn--96j"], + ["\u3015", "xn--b7j"], + ["\u3033", "xn--57j"], + ["\u3164", "xn--psd"], + ["\u321D", "xn--()-357j35d"], + ["\u321E", "xn--()-357jf36c"], + ["\u33AE", "xn--rads-id9a"], + ["\u33AF", "xn--rads2-4d6b"], + ["\u33C6", "xn--ckg-tc2a"], + ["\u33DF", "xn--am-6bv"], + ["\uA789", "xn--058a"], + ["\uFE3F", "xn--x6j"], + ["\uFE5D", "xn--96j"], + ["\uFE5E", "xn--b7j"], + ["\uFFA0", "xn--psd"], + ["\uFFF9", "xn--vn7c"], + ["\uFFFA", "xn--wn7c"], + ["\uFFFB", "xn--xn7c"], + ["\uFFFC", "xn--yn7c"], + ["\uFFFD", "xn--zn7c"], + + // Characters from the IDN blacklist that normalize to ASCII + // If we start using STD3ASCIIRules these will be blocked (bug 316444) + ["\u00A0", " "], + ["\u2000", " "], + ["\u2001", " "], + ["\u2002", " "], + ["\u2003", " "], + ["\u2004", " "], + ["\u2005", " "], + ["\u2006", " "], + ["\u2007", " "], + ["\u2008", " "], + ["\u2009", " "], + ["\u200A", " "], + ["\u2024", "."], + ["\u202F", " "], + ["\u205F", " "], + ["\u3000", " "], + ["\u3002", "."], + ["\uFE14", ";"], + ["\uFE15", "!"], + ["\uFF0E", "."], + ["\uFF0F", "/"], + ["\uFF61", "."], + + // Characters from the IDN blacklist that are stripped by Nameprep + ["\u200B", ""], + ["\uFEFF", ""], +]; + +function run_test() { + var pbi = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + var oldProfile = pbi.getCharPref( + "network.IDN.restriction_profile", + "moderate" + ); + var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false); + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + pbi.setCharPref("network.IDN.restriction_profile", "moderate"); + pbi.setBoolPref("network.IDN.whitelist.com", false); + + for (var j = 0; j < testcases.length; ++j) { + var test = testcases[j]; + var URL = test[0] + ".com"; + var punycodeURL = test[1] + ".com"; + var isASCII = {}; + + var result; + try { + result = idnService.convertToDisplayIDN(URL, isASCII); + } catch (e) { + result = ".com"; + } + // If the punycode URL is equivalent to \ufffd.com (i.e. the + // blacklisted character has been replaced by a unicode + // REPLACEMENT CHARACTER, skip the test + if (result != "xn--zn7c.com") { + if (punycodeURL.substr(0, 4) == "xn--") { + // test convertToDisplayIDN with a Unicode URL and with a + // Punycode URL if we have one + equal(escape(result), escape(punycodeURL)); + + result = idnService.convertToDisplayIDN(punycodeURL, isASCII); + equal(escape(result), escape(punycodeURL)); + } else { + // The "punycode" URL isn't punycode. This happens in testcases + // where the Unicode URL has become normalized to an ASCII URL, + // so, even though expectedUnicode is true, the expected result + // is equal to punycodeURL + equal(escape(result), escape(punycodeURL)); + } + } + } + pbi.setBoolPref("network.IDN.whitelist.com", oldWhitelistCom); + pbi.setCharPref("network.IDN.restriction_profile", oldProfile); +} diff --git a/netwerk/test/unit/test_idn_urls.js b/netwerk/test/unit/test_idn_urls.js new file mode 100644 index 0000000000..2e00ca6932 --- /dev/null +++ b/netwerk/test/unit/test_idn_urls.js @@ -0,0 +1,441 @@ +// Test algorithm for unicode display of IDNA URL (bug 722299) + +"use strict"; + +const testcases = [ + // Original Punycode or Expected UTF-8 by profile + // URL normalized form ASCII-Only, High, Moderate + // + // Latin script + ["cuillère", "xn--cuillre-6xa", false, true, true], + + // repeated non-spacing marks + ["gruz̀̀ere", "xn--gruzere-ogea", false, false, false], + + // non-XID character + ["I♥NY", "xn--iny-zx5a", false, false, false], + + /* + Behaviour of this test changed in IDNA2008, replacing the non-XID + character with U+FFFD replacement character - when all platforms use + IDNA2008 it can be uncommented and the punycode URL changed to + "xn--mgbl3eb85703a" + + // new non-XID character in Unicode 6.3 + ["حلا\u061cل", "xn--bgbvr6gc", false, false, false], +*/ + + // U+30FB KATAKANA MIDDLE DOT is excluded from non-XID characters (bug 857490) + ["乾燥肌・石けん", "xn--08j4gylj12hz80b0uhfup", false, true, true], + + // Cyrillic alone + ["толсто́й", "xn--lsa83dealbred", false, true, true], + + // Mixed script Cyrillic/Latin + [ + "толсто́й-in-Russian", + "xn---in-russian-1jg071b0a8bb4cpd", + false, + false, + false, + ], + + // Mixed script Latin/Cyrillic + ["war-and-миръ", "xn--war-and--b9g3b7b3h", false, false, false], + + // Cherokee (Restricted script) + ["ᏣᎳᎩ", "xn--f9dt7l", false, false, false], + + // Yi (former Aspirational script, now Restricted per Unicode 10.0 update to UAX 31) + ["ꆈꌠꁱꂷ", "xn--4o7a6e1x64c", false, false, false], + + // Greek alone + ["πλάτων", "xn--hxa3ahjw4a", false, true, true], + + // Mixed script Greek/Latin + [ + "πλάτωνicrelationship", + "xn--icrelationship-96j4t9a3cwe2e", + false, + false, + false, + ], + + // Mixed script Latin/Greek + ["spaceὈδύσσεια", "xn--space-h9dui0b0ga2j1562b", false, false, false], + + // Devanagari alone + ["मराठी", "xn--d2b1ag0dl", false, true, true], + + // Devanagari with Armenian + ["मराठीՀայաստան", "xn--y9aaa1d0ai1cq964f8dwa2o1a", false, false, false], + + // Devanagari with common + ["मराठी123", "xn--123-mhh3em2hra", false, true, true], + + // Common with Devanagari + ["123मराठी", "xn--123-phh3em2hra", false, true, true], + + // Latin with Han + ["chairman毛", "xn--chairman-k65r", false, true, true], + + // Han with Latin + ["山葵sauce", "xn--sauce-6j9ii40v", false, true, true], + + // Latin with Han, Hiragana and Katakana + ["van語ではドイ", "xn--van-ub4bpb6w0in486d", false, true, true], + + // Latin with Han, Katakana and Hiragana + ["van語ドイでは", "xn--van-ub4bpb4w0ip486d", false, true, true], + + // Latin with Hiragana, Han and Katakana + ["vanでは語ドイ", "xn--van-ub4bpb6w0ip486d", false, true, true], + + // Latin with Hiragana, Katakana and Han + ["vanではドイ語", "xn--van-ub4bpb6w0ir486d", false, true, true], + + // Latin with Katakana, Han and Hiragana + ["vanドイ語では", "xn--van-ub4bpb4w0ir486d", false, true, true], + + // Latin with Katakana, Hiragana and Han + ["vanドイでは語", "xn--van-ub4bpb4w0it486d", false, true, true], + + // Han with Latin, Hiragana and Katakana + ["語vanではドイ", "xn--van-ub4bpb6w0ik486d", false, true, true], + + // Han with Latin, Katakana and Hiragana + ["語vanドイでは", "xn--van-ub4bpb4w0im486d", false, true, true], + + // Han with Hiragana, Latin and Katakana + ["語ではvanドイ", "xn--van-rb4bpb9w0ik486d", false, true, true], + + // Han with Hiragana, Katakana and Latin + ["語ではドイvan", "xn--van-rb4bpb6w0in486d", false, true, true], + + // Han with Katakana, Latin and Hiragana + ["語ドイvanでは", "xn--van-ub4bpb1w0ip486d", false, true, true], + + // Han with Katakana, Hiragana and Latin + ["語ドイではvan", "xn--van-rb4bpb4w0ip486d", false, true, true], + + // Hiragana with Latin, Han and Katakana + ["イツvan語ではド", "xn--van-ub4bpb1wvhsbx330n", false, true, true], + + // Hiragana with Latin, Katakana and Han + ["ではvanドイ語", "xn--van-rb4bpb9w0ir486d", false, true, true], + + // Hiragana with Han, Latin and Katakana + ["では語vanドイ", "xn--van-rb4bpb9w0im486d", false, true, true], + + // Hiragana with Han, Katakana and Latin + ["では語ドイvan", "xn--van-rb4bpb6w0ip486d", false, true, true], + + // Hiragana with Katakana, Latin and Han + ["ではドイvan語", "xn--van-rb4bpb6w0iu486d", false, true, true], + + // Hiragana with Katakana, Han and Latin + ["ではドイ語van", "xn--van-rb4bpb6w0ir486d", false, true, true], + + // Katakana with Latin, Han and Hiragana + ["ドイvan語では", "xn--van-ub4bpb1w0iu486d", false, true, true], + + // Katakana with Latin, Hiragana and Han + ["ドイvanでは語", "xn--van-ub4bpb1w0iw486d", false, true, true], + + // Katakana with Han, Latin and Hiragana + ["ドイ語vanでは", "xn--van-ub4bpb1w0ir486d", false, true, true], + + // Katakana with Han, Hiragana and Latin + ["ドイ語ではvan", "xn--van-rb4bpb4w0ir486d", false, true, true], + + // Katakana with Hiragana, Latin and Han + ["ドイではvan語", "xn--van-rb4bpb4w0iw486d", false, true, true], + + // Katakana with Hiragana, Han and Latin + ["ドイでは語van", "xn--van-rb4bpb4w0it486d", false, true, true], + + // Han with common + ["中国123", "xn--123-u68dy61b", false, true, true], + + // common with Han + ["123中国", "xn--123-x68dy61b", false, true, true], + + // Characters that normalize to permitted characters + // (also tests Plane 1 supplementary characters) + ["super𝟖", "super8", true, true, true], + + // Han from Plane 2 + ["𠀀𠀁𠀂", "xn--j50icd", false, true, true], + + // Han from Plane 2 with js (UTF-16) escapes + ["\uD840\uDC00\uD840\uDC01\uD840\uDC02", "xn--j50icd", false, true, true], + + // Same with a lone high surrogate at the end + ["\uD840\uDC00\uD840\uDC01\uD840", "xn--zn7c0336bda", false, false, false], + + // Latin text and Bengali digits + ["super৪", "xn--super-k2l", false, false, true], + + // Bengali digits and Latin text + ["৫ab", "xn--ab-x5f", false, false, true], + + // Bengali text and Latin digits + ["অঙ্কুর8", "xn--8-70d2cp0j6dtd", false, true, true], + + // Latin digits and Bengali text + ["5াব", "xn--5-h3d7c", false, true, true], + + // Mixed numbering systems + ["٢٠۰٠", "xn--8hbae38c", false, false, false], + + // Traditional Chinese + ["萬城", "xn--uis754h", false, true, true], + + // Simplified Chinese + ["万城", "xn--chq31v", false, true, true], + + // Simplified-only and Traditional-only Chinese in the same label + ["万萬城", "xn--chq31vsl1b", false, true, true], + + // Traditional-only and Simplified-only Chinese in the same label + ["萬万城", "xn--chq31vrl1b", false, true, true], + + // Han and Latin and Bopomofo + [ + "注音符号bopomofoㄅㄆㄇㄈ", + "xn--bopomofo-hj5gkalm1637i876cuw0brk5f", + false, + true, + true, + ], + + // Han, bopomofo, Latin + [ + "注音符号ㄅㄆㄇㄈbopomofo", + "xn--bopomofo-8i5gkalm9637i876cuw0brk5f", + false, + true, + true, + ], + + // Latin, Han, Bopomofo + [ + "bopomofo注音符号ㄅㄆㄇㄈ", + "xn--bopomofo-hj5gkalm9637i876cuw0brk5f", + false, + true, + true, + ], + + // Latin, Bopomofo, Han + [ + "bopomofoㄅㄆㄇㄈ注音符号", + "xn--bopomofo-hj5gkalm3737i876cuw0brk5f", + false, + true, + true, + ], + + // Bopomofo, Han, Latin + [ + "ㄅㄆㄇㄈ注音符号bopomofo", + "xn--bopomofo-8i5gkalm3737i876cuw0brk5f", + false, + true, + true, + ], + + // Bopomofo, Latin, Han + [ + "ㄅㄆㄇㄈbopomofo注音符号", + "xn--bopomofo-8i5gkalm1837i876cuw0brk5f", + false, + true, + true, + ], + + // Han, bopomofo and katakana + [ + "注音符号ㄅㄆㄇㄈボポモフォ", + "xn--jckteuaez1shij0450gylvccz9asi4e", + false, + false, + false, + ], + + // Han, katakana, bopomofo + [ + "注音符号ボポモフォㄅㄆㄇㄈ", + "xn--jckteuaez6shij5350gylvccz9asi4e", + false, + false, + false, + ], + + // bopomofo, han, katakana + [ + "ㄅㄆㄇㄈ注音符号ボポモフォ", + "xn--jckteuaez1shij4450gylvccz9asi4e", + false, + false, + false, + ], + + // bopomofo, katakana, han + [ + "ㄅㄆㄇㄈボポモフォ注音符号", + "xn--jckteuaez1shij9450gylvccz9asi4e", + false, + false, + false, + ], + + // katakana, Han, bopomofo + [ + "ボポモフォ注音符号ㄅㄆㄇㄈ", + "xn--jckteuaez6shij0450gylvccz9asi4e", + false, + false, + false, + ], + + // katakana, bopomofo, Han + [ + "ボポモフォㄅㄆㄇㄈ注音符号", + "xn--jckteuaez6shij4450gylvccz9asi4e", + false, + false, + false, + ], + + // Han, Hangul and Latin + ["韓한글hangul", "xn--hangul-2m5ti09k79ze", false, true, true], + + // Han, Latin and Hangul + ["韓hangul한글", "xn--hangul-2m5to09k79ze", false, true, true], + + // Hangul, Han and Latin + ["한글韓hangul", "xn--hangul-2m5th09k79ze", false, true, true], + + // Hangul, Latin and Han + ["한글hangul韓", "xn--hangul-8m5t898k79ze", false, true, true], + + // Latin, Han and Hangul + ["hangul韓한글", "xn--hangul-8m5ti09k79ze", false, true, true], + + // Latin, Hangul and Han + ["hangul한글韓", "xn--hangul-8m5th09k79ze", false, true, true], + + // Hangul and katakana + ["한글ハングル", "xn--qck1c2d4a9266lkmzb", false, false, false], + + // Katakana and Hangul + ["ハングル한글", "xn--qck1c2d4a2366lkmzb", false, false, false], + + // Thai (also tests that node with over 63 UTF-8 octets doesn't fail) + [ + "เครื่องทําน้ําทําน้ําแข็ง", + "xn--22cdjb2fanb9fyepcbbb9dwh4a3igze4fdcd", + false, + true, + true, + ], + + // Effect of adding valid or invalid subdomains (bug 1399540) + ["䕮䕵䕶䕱.ascii", "xn--google.ascii", false, true, true], + ["ascii.䕮䕵䕶䕱", "ascii.xn--google", false, true, true], + ["中国123.䕮䕵䕶䕱", "xn--123-u68dy61b.xn--google", false, true, true], + ["䕮䕵䕶䕱.中国123", "xn--google.xn--123-u68dy61b", false, true, true], + [ + "xn--accountlogin.䕮䕵䕶䕱", + "xn--accountlogin.xn--google", + false, + true, + true, + ], + [ + "䕮䕵䕶䕱.xn--accountlogin", + "xn--google.xn--accountlogin", + false, + true, + true, + ], + + // Arabic diacritic not allowed in Latin text (bug 1370497) + ["goo\u0650gle", "xn--google-yri", false, false, false], + // ...but Arabic diacritics are allowed on Arabic text + ["العَرَبِي", "xn--mgbc0a5a6cxbzabt", false, true, true], + + // Hebrew diacritic also not allowed in Latin text (bug 1404349) + ["goo\u05b4gle", "xn--google-rvh", false, false, false], + + // Accents above dotless-i are not allowed + ["na\u0131\u0308ve", "xn--nave-mza04z", false, false, false], + ["d\u0131\u0302ner", "xn--dner-lza40z", false, false, false], + // but the corresponding accented-i (based on dotted i) is OK + ["na\u00efve.com", "xn--nave-6pa.com", false, true, true], + ["d\u00eener.com", "xn--dner-0pa.com", false, true, true], +]; + +const profiles = ["ASCII", "high", "moderate"]; + +function run_test() { + var pbi = Cc["@mozilla.org/preferences-service;1"].getService( + Ci.nsIPrefBranch + ); + var oldProfile = pbi.getCharPref( + "network.IDN.restriction_profile", + "moderate" + ); + var oldWhitelistCom = pbi.getBoolPref("network.IDN.whitelist.com", false); + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + for (var i = 0; i < profiles.length; ++i) { + pbi.setCharPref("network.IDN.restriction_profile", profiles[i]); + pbi.setBoolPref("network.IDN.whitelist.com", false); + + dump("testing " + profiles[i] + " profile"); + + for (var j = 0; j < testcases.length; ++j) { + var test = testcases[j]; + var URL = test[0] + ".com"; + var punycodeURL = test[1] + ".com"; + var expectedUnicode = test[2 + i]; + var isASCII = {}; + + var result; + try { + result = idnService.convertToDisplayIDN(URL, isASCII); + } catch (e) { + result = ".com"; + } + if ( + punycodeURL.substr(0, 4) == "xn--" || + punycodeURL.indexOf(".xn--") > 0 + ) { + // test convertToDisplayIDN with a Unicode URL and with a + // Punycode URL if we have one + Assert.equal( + escape(result), + expectedUnicode ? escape(URL) : escape(punycodeURL) + ); + + result = idnService.convertToDisplayIDN(punycodeURL, isASCII); + Assert.equal( + escape(result), + expectedUnicode ? escape(URL) : escape(punycodeURL) + ); + } else { + // The "punycode" URL isn't punycode. This happens in testcases + // where the Unicode URL has become normalized to an ASCII URL, + // so, even though expectedUnicode is true, the expected result + // is equal to punycodeURL + Assert.equal(escape(result), escape(punycodeURL)); + } + } + } + pbi.setBoolPref("network.IDN.whitelist.com", oldWhitelistCom); + pbi.setCharPref("network.IDN.restriction_profile", oldProfile); +} diff --git a/netwerk/test/unit/test_idna2008.js b/netwerk/test/unit/test_idna2008.js new file mode 100644 index 0000000000..0e0290ce79 --- /dev/null +++ b/netwerk/test/unit/test_idna2008.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const kTransitionalProcessing = false; + +// Four characters map differently under non-transitional processing: +const labels = [ + // U+00DF LATIN SMALL LETTER SHARP S to "ss" + "stra\u00dfe", + // U+03C2 GREEK SMALL LETTER FINAL SIGMA to U+03C3 GREEK SMALL LETTER SIGMA + "\u03b5\u03bb\u03bb\u03ac\u03c2", + // U+200C ZERO WIDTH NON-JOINER in Indic script + "\u0646\u0627\u0645\u0647\u200c\u0627\u06cc", + // U+200D ZERO WIDTH JOINER in Arabic script + "\u0dc1\u0dca\u200d\u0dbb\u0dd3", + + // But CONTEXTJ rules prohibit ZWJ and ZWNJ in non-Arabic or Indic scripts + // U+200C ZERO WIDTH NON-JOINER in Latin script + "m\u200cn", + // U+200D ZERO WIDTH JOINER in Latin script + "p\u200dq", +]; + +const transitionalExpected = [ + "strasse", + "xn--hxarsa5b", + "xn--mgba3gch31f", + "xn--10cl1a0b", + "", + "", +]; + +const nonTransitionalExpected = [ + "xn--strae-oqa", + "xn--hxarsa0b", + "xn--mgba3gch31f060k", + "xn--10cl1a0b660p", + "", + "", +]; + +// Test options for converting IDN URLs under IDNA2008 +function run_test() { + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + for (var i = 0; i < labels.length; ++i) { + var result; + try { + result = idnService.convertUTF8toACE(labels[i]); + } catch (e) { + result = ""; + } + + if (kTransitionalProcessing) { + equal(result, transitionalExpected[i]); + } else { + equal(result, nonTransitionalExpected[i]); + } + } +} diff --git a/netwerk/test/unit/test_idnservice.js b/netwerk/test/unit/test_idnservice.js new file mode 100644 index 0000000000..0c52f300e3 --- /dev/null +++ b/netwerk/test/unit/test_idnservice.js @@ -0,0 +1,39 @@ +// Tests nsIIDNService + +"use strict"; + +const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService +); + +add_task(async function test_simple() { + let reference = [ + // The 3rd element indicates whether the second element + // is ACE-encoded + ["asciihost", "asciihost", false], + ["b\u00FCcher", "xn--bcher-kva", true], + ]; + + for (var i = 0; i < reference.length; ++i) { + dump("Testing " + reference[i] + "\n"); + // We test the following: + // - Converting UTF-8 to ACE and back gives us the expected answer + // - Converting the ASCII string UTF-8 -> ACE leaves the string unchanged + // - isACE returns true when we expect it to (third array elem true) + Assert.equal(idnService.convertUTF8toACE(reference[i][0]), reference[i][1]); + Assert.equal(idnService.convertUTF8toACE(reference[i][1]), reference[i][1]); + Assert.equal(idnService.convertACEtoUTF8(reference[i][1]), reference[i][0]); + Assert.equal(idnService.isACE(reference[i][1]), reference[i][2]); + } +}); + +add_task(async function test_extra_blocked() { + let isAscii = {}; + equal(idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii), "goșu.ro"); + Services.prefs.setStringPref("network.IDN.extra_blocked_chars", "ș"); + equal( + idnService.convertToDisplayIDN("xn--gou-2lb.ro", isAscii), + "xn--gou-2lb.ro" + ); + Services.prefs.clearUserPref("network.IDN.extra_blocked_chars"); +}); diff --git a/netwerk/test/unit/test_immutable.js b/netwerk/test/unit/test_immutable.js new file mode 100644 index 0000000000..50b7967a6a --- /dev/null +++ b/netwerk/test/unit/test_immutable.js @@ -0,0 +1,167 @@ +"use strict"; + +var prefs; +var spdypref; +var http2pref; +var origin; +var rcwnpref; + +function run_test() { + var env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + var h2Port = env.get("MOZHTTP2_PORT"); + Assert.notEqual(h2Port, null); + Assert.notEqual(h2Port, ""); + + // Set to allow the cert presented by our H2 server + do_get_profile(); + prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + + spdypref = prefs.getBoolPref("network.http.spdy.enabled"); + http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2"); + rcwnpref = prefs.getBoolPref("network.http.rcwn.enabled"); + + prefs.setBoolPref("network.http.spdy.enabled", true); + prefs.setBoolPref("network.http.spdy.enabled.http2", true); + prefs.setCharPref( + "network.dns.localDomains", + "foo.example.com, bar.example.com" + ); + // Disable rcwn to make cache behavior deterministic. + prefs.setBoolPref("network.http.rcwn.enabled", false); + + // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem + // so add that cert to the trust list as a signing cert. // the foo.example.com domain name. + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + origin = "https://foo.example.com:" + h2Port; + dump("origin - " + origin + "\n"); + doTest1(); +} + +function resetPrefs() { + prefs.setBoolPref("network.http.spdy.enabled", spdypref); + prefs.setBoolPref("network.http.spdy.enabled.http2", http2pref); + prefs.setBoolPref("network.http.rcwn.enabled", rcwnpref); + prefs.clearUserPref("network.dns.localDomains"); +} + +function makeChan(origin, path) { + return NetUtil.newChannel({ + uri: origin + path, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +var nextTest; +var expectPass = true; +var expectConditional = false; + +var Listener = function() {}; +Listener.prototype = { + onStartRequest: function testOnStartRequest(request) { + Assert.ok(request instanceof Ci.nsIHttpChannel); + + if (expectPass) { + if (!Components.isSuccessCode(request.status)) { + do_throw( + "Channel should have a success code! (" + request.status + ")" + ); + } + Assert.equal(request.responseStatus, 200); + } else { + Assert.equal(Components.isSuccessCode(request.status), false); + } + }, + + onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) { + read_stream(stream, cnt); + }, + + onStopRequest: function testOnStopRequest(request, status) { + if (expectConditional) { + Assert.equal(request.getResponseHeader("x-conditional"), "true"); + } else { + try { + Assert.notEqual(request.getResponseHeader("x-conditional"), "true"); + } catch (e) { + Assert.ok(true); + } + } + nextTest(); + do_test_finished(); + }, +}; + +function testsDone() { + dump("testDone\n"); + resetPrefs(); +} + +function doTest1() { + dump("execute doTest1 - resource without immutable. initial request\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest2; + chan.asyncOpen(listener); +} + +function doTest2() { + dump("execute doTest2 - resource without immutable. reload\n"); + do_test_pending(); + expectConditional = true; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest3; + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function doTest3() { + dump("execute doTest3 - resource without immutable. shift reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-without-attribute"); + var listener = new Listener(); + nextTest = doTest4; + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(listener); +} + +function doTest4() { + dump("execute doTest1 - resource with immutable. initial request\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = doTest5; + chan.asyncOpen(listener); +} + +function doTest5() { + dump("execute doTest5 - resource with immutable. reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = doTest6; + chan.loadFlags = Ci.nsIRequest.VALIDATE_ALWAYS; + chan.asyncOpen(listener); +} + +function doTest6() { + dump("execute doTest3 - resource with immutable. shift reload\n"); + do_test_pending(); + expectConditional = false; + var chan = makeChan(origin, "/immutable-test-with-attribute"); + var listener = new Listener(); + nextTest = testsDone; + chan.loadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE; + chan.asyncOpen(listener); +} diff --git a/netwerk/test/unit/test_inhibit_caching.js b/netwerk/test/unit/test_inhibit_caching.js new file mode 100644 index 0000000000..1af75aaa33 --- /dev/null +++ b/netwerk/test/unit/test_inhibit_caching.js @@ -0,0 +1,92 @@ +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var first = true; +function contentHandler(metadata, response) { + response.setHeader("Content-Type", "text/plain"); + var body = "first"; + if (!first) { + body = "second"; + } + first = false; + response.bodyOutputStream.write(body, body.length); +} + +XPCOMUtils.defineLazyGetter(this, "uri", function() { + return "http://localhost:" + httpserver.identity.primaryPort; +}); + +var httpserver = null; + +function run_test() { + // setup test + httpserver = new HttpServer(); + httpserver.registerPathHandler("/test", contentHandler); + httpserver.start(-1); + + add_test(test_first_response); + add_test(test_inhibit_caching); + + run_next_test(); +} + +// Makes a regular request +function test_first_response() { + var chan = NetUtil.newChannel({ + uri: uri + "/test", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(new ChannelListener(check_first_response, null)); +} + +// Checks that we got the appropriate response +function check_first_response(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(buffer, "first"); + // Open the cache entry to check its contents + asyncOpenCacheEntry( + uri + "/test", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + cache_entry_callback + ); +} + +// Checks that the cache entry has the correct contents +function cache_entry_callback(status, entry) { + equal(status, Cr.NS_OK); + var inputStream = entry.openInputStream(0); + pumpReadStream(inputStream, function(read) { + inputStream.close(); + equal(read, "first"); + run_next_test(); + }); +} + +// Makes a request with the INHIBIT_CACHING load flag +function test_inhibit_caching() { + var chan = NetUtil.newChannel({ + uri: uri + "/test", + loadUsingSystemPrincipal: true, + }); + chan.QueryInterface(Ci.nsIRequest).loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + chan.asyncOpen(new ChannelListener(check_second_response, null)); +} + +// Checks that we got a different response from the first request +function check_second_response(request, buffer) { + request.QueryInterface(Ci.nsIHttpChannel); + Assert.equal(request.responseStatus, 200); + Assert.equal(buffer, "second"); + // Checks that the cache entry still contains the content from the first request + asyncOpenCacheEntry( + uri + "/test", + "disk", + Ci.nsICacheStorage.OPEN_READONLY, + null, + cache_entry_callback + ); +} diff --git a/netwerk/test/unit/test_ioservice.js b/netwerk/test/unit/test_ioservice.js new file mode 100644 index 0000000000..d218d77a7a --- /dev/null +++ b/netwerk/test/unit/test_ioservice.js @@ -0,0 +1,19 @@ +"use strict"; + +add_task(function test_extractScheme() { + equal(Services.io.extractScheme("HtTp://example.com"), "http"); + Assert.throws( + () => { + Services.io.extractScheme("://example.com"); + }, + /NS_ERROR_MALFORMED_URI/, + "missing scheme" + ); + Assert.throws( + () => { + Services.io.extractScheme("ht%tp://example.com"); + }, + /NS_ERROR_MALFORMED_URI/, + "bad scheme" + ); +}); diff --git a/netwerk/test/unit/test_large_port.js b/netwerk/test/unit/test_large_port.js new file mode 100644 index 0000000000..492a03047f --- /dev/null +++ b/netwerk/test/unit/test_large_port.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Ensure that non-16-bit URIs are rejected + +"use strict"; + +function run_test() { + let mutator = Cc[ + "@mozilla.org/network/standard-url-mutator;1" + ].createInstance(Ci.nsIURIMutator); + Assert.ok(mutator, "Mutator constructor works"); + + let url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init( + Ci.nsIStandardURL.URLTYPE_AUTHORITY, + 65535, + "http://localhost", + "UTF-8", + null + ) + .finalize(); + + // Bug 1301621 makes invalid ports throw + Assert.throws( + () => { + url = Cc["@mozilla.org/network/standard-url-mutator;1"] + .createInstance(Ci.nsIStandardURLMutator) + .init( + Ci.nsIStandardURL.URLTYPE_AUTHORITY, + 65536, + "http://localhost", + "UTF-8", + null + ) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port during creation" + ); + + Assert.throws( + () => { + url = url + .mutate() + .QueryInterface(Ci.nsIStandardURLMutator) + .setDefaultPort(65536) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port in setDefaultPort" + ); + Assert.throws( + () => { + url = url + .mutate() + .setPort(65536) + .finalize(); + }, + /NS_ERROR_MALFORMED_URI/, + "invalid port in port setter" + ); + + Assert.equal(url.port, -1); + do_test_finished(); +} diff --git a/netwerk/test/unit/test_link.desktop b/netwerk/test/unit/test_link.desktop new file mode 100644 index 0000000000..b1798202e3 --- /dev/null +++ b/netwerk/test/unit/test_link.desktop @@ -0,0 +1,3 @@ +[Desktop Entry] +Type=Link +URL=http://www.mozilla.org/ diff --git a/netwerk/test/unit/test_link.lnk b/netwerk/test/unit/test_link.lnk new file mode 100644 index 0000000000..125d859f43 Binary files /dev/null and b/netwerk/test/unit/test_link.lnk differ diff --git a/netwerk/test/unit/test_link.url b/netwerk/test/unit/test_link.url new file mode 100644 index 0000000000..05f8275544 --- /dev/null +++ b/netwerk/test/unit/test_link.url @@ -0,0 +1,5 @@ +[InternetShortcut] +URL=http://www.mozilla.org/ +IDList= +[{000214A0-0000-0000-C000-000000000046}] +Prop3=19,2 diff --git a/netwerk/test/unit/test_loadgroup_cancel.js b/netwerk/test/unit/test_loadgroup_cancel.js new file mode 100644 index 0000000000..accfede36a --- /dev/null +++ b/netwerk/test/unit/test_loadgroup_cancel.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function makeChan(url) { + return NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + }).QueryInterface(Ci.nsIHttpChannel); +} + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +function request_handler(metadata, response) { + response.processAsync(); + do_timeout(500, () => { + const body = "some body once told me..."; + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("Content-Length", "" + body.length, false); + response.bodyOutputStream.write(body, body.length); + response.finish(); + }); +} + +// This test checks that when canceling a loadgroup by the time the loadgroup's +// groupObserver is sent OnStopRequest for a request, that request has been +// canceled. +add_task(async function test_cancelledInOnStop() { + let http_server = new HttpServer(); + http_server.registerPathHandler("/test1", request_handler); + http_server.registerPathHandler("/test2", request_handler); + http_server.registerPathHandler("/test3", request_handler); + http_server.start(-1); + const port = http_server.identity.primaryPort; + + let loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + let loadListener = { + onStartRequest: aRequest => { + info("onStartRequest"); + }, + onStopRequest: (aRequest, aStatusCode) => { + equal( + aStatusCode, + Cr.NS_ERROR_ABORT, + "aStatusCode must be the cancellation code" + ); + equal( + aRequest.status, + Cr.NS_ERROR_ABORT, + "aRequest.status must be the cancellation code" + ); + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsISupportsWeakReference", + ]), + }; + loadGroup.groupObserver = loadListener; + + let chan1 = makeChan(`http://localhost:${port}/test1`); + chan1.loadGroup = loadGroup; + let chan2 = makeChan(`http://localhost:${port}/test2`); + chan2.loadGroup = loadGroup; + let chan3 = makeChan(`http://localhost:${port}/test3`); + chan3.loadGroup = loadGroup; + + await new Promise(resolve => do_timeout(500, resolve)); + + let promises = [ + new Promise(resolve => { + chan1.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + new Promise(resolve => { + chan2.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + new Promise(resolve => { + chan3.asyncOpen(new ChannelListener(resolve, null, CL_EXPECT_FAILURE)); + }), + ]; + + loadGroup.cancel(Cr.NS_ERROR_ABORT); + + await Promise.all(promises); + + await new Promise(resolve => { + http_server.stop(resolve); + }); +}); diff --git a/netwerk/test/unit/test_localstreams.js b/netwerk/test/unit/test_localstreams.js new file mode 100644 index 0000000000..a4b76a7668 --- /dev/null +++ b/netwerk/test/unit/test_localstreams.js @@ -0,0 +1,90 @@ +// Tests bug 304414 + +"use strict"; + +const PR_RDONLY = 0x1; // see prio.h + +// Does some sanity checks on the stream and returns the number of bytes read +// when the checks passed. +function test_stream(stream) { + // This test only handles blocking streams; that's desired for file streams + // anyway. + Assert.equal(stream.isNonBlocking(), false); + + // Check that the stream is not buffered + Assert.equal( + Cc["@mozilla.org/io-util;1"] + .getService(Ci.nsIIOUtil) + .inputStreamIsBuffered(stream), + false + ); + + // Wrap it in a binary stream (to avoid wrong results that + // scriptablestream would produce with binary content) + var binstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binstream.setInputStream(stream); + + var numread = 0; + for (;;) { + Assert.equal(stream.available(), binstream.available()); + var avail = stream.available(); + Assert.notEqual(avail, -1); + + // PR_UINT32_MAX and PR_INT32_MAX; the files we're testing with aren't that + // large. + Assert.notEqual(avail, Math.pow(2, 32) - 1); + Assert.notEqual(avail, Math.pow(2, 31) - 1); + + if (!avail) { + // For blocking streams, available() only returns 0 on EOF + // Make sure that there is really no data left + var could_read = false; + try { + binstream.readByteArray(1); + could_read = true; + } catch (e) { + // We expect the exception, so do nothing here + } + if (could_read) { + do_throw("Data readable when available indicated EOF!"); + } + return numread; + } + + dump("Trying to read " + avail + " bytes\n"); + // Note: Verification that this does return as much bytes as we asked for is + // done in the binarystream implementation + binstream.readByteArray(avail); + + numread += avail; + } +} + +function stream_for_file(file) { + var str = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + str.init(file, PR_RDONLY, 0, 0); + return str; +} + +function stream_from_channel(file) { + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var uri = ios.newFileURI(file); + return NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + }).open(); +} + +function run_test() { + // Get a file and a directory in order to do some testing + var file = do_get_file("../unit/data/test_readline6.txt"); + var len = file.fileSize; + Assert.equal(test_stream(stream_for_file(file)), len); + Assert.equal(test_stream(stream_from_channel(file)), len); + var dir = file.parent; + test_stream(stream_from_channel(dir)); // Can't do size checking +} diff --git a/netwerk/test/unit/test_mismatch_last-modified.js b/netwerk/test/unit/test_mismatch_last-modified.js new file mode 100644 index 0000000000..5b59464b53 --- /dev/null +++ b/netwerk/test/unit/test_mismatch_last-modified.js @@ -0,0 +1,155 @@ +"use strict"; + +const BinaryInputStream = Components.Constructor( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +var httpserver = new HttpServer(); + +var ios; + +// Test the handling of a cache revalidation with mismatching last-modified +// headers. If we get such a revalidation the cache entry should be purged. +// see bug 717350 + +// In this test the wrong data is from 11-16-1994 with a value of 'A', +// and the right data is from 11-15-1994 with a value of 'B'. + +// the same URL is requested 3 times. the first time the wrong data comes +// back, the second time that wrong data is revalidated with a 304 but +// a L-M header of the right data (this triggers a cache purge), and +// the third time the right data is returned. + +var listener_3 = { + // this listener is used to process the the request made after + // the cache invalidation. it expects to see the 'right data' + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + + Assert.equal(data[0], "B".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + httpserver.stop(do_test_finished); + }, +}; + +XPCOMUtils.defineLazyGetter(this, "listener_2", function() { + return { + // this listener is used to process the revalidation of the + // corrupted cache entry. its revalidation prompts it to be cleaned + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + + // This is 'A' from a cache revalidation, but that reval will clean the cache + // because of mismatched last-modified response headers + + Assert.equal(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_3); + }, + }; +}); + +XPCOMUtils.defineLazyGetter(this, "listener_1", function() { + return { + // this listener processes the initial request from a empty cache. + // the server responds with the wrong data ('A') + + QueryInterface: ChromeUtils.generateQI([ + "nsIStreamListener", + "nsIRequestObserver", + ]), + + onStartRequest: function test_onStartR(request) {}, + + onDataAvailable: function test_ODA(request, inputStream, offset, count) { + var data = new BinaryInputStream(inputStream).readByteArray(count); + Assert.equal(data[0], "A".charCodeAt(0)); + }, + + onStopRequest: function test_onStopR(request, status) { + request.QueryInterface(Ci.nsIHttpChannel); + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + httpserver.identity.primaryPort + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_2); + }, + }; +}); + +function run_test() { + do_get_profile(); + ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + + evict_cache_entries(); + + httpserver.registerPathHandler("/test1", handler); + httpserver.start(-1); + + var port = httpserver.identity.primaryPort; + var chan = NetUtil.newChannel({ + uri: "http://localhost:" + port + "/test1", + loadUsingSystemPrincipal: true, + }); + chan.asyncOpen(listener_1); + + do_test_pending(); +} + +var iter = 0; +function handler(metadata, response) { + iter++; + if (metadata.hasHeader("If-Modified-Since")) { + response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); + response.setHeader("Last-Modified", "Tue, 15 Nov 1994 12:45:26 GMT", false); + } else { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "max-age=0", false); + if (iter == 1) { + // simulated wrong response + response.setHeader( + "Last-Modified", + "Wed, 16 Nov 1994 00:00:00 GMT", + false + ); + response.bodyOutputStream.write("A", 1); + } + if (iter == 3) { + // 'correct' response + response.setHeader( + "Last-Modified", + "Tue, 15 Nov 1994 12:45:26 GMT", + false + ); + response.bodyOutputStream.write("B", 1); + } + } +} diff --git a/netwerk/test/unit/test_mozTXTToHTMLConv.js b/netwerk/test/unit/test_mozTXTToHTMLConv.js new file mode 100644 index 0000000000..620d527e1c --- /dev/null +++ b/netwerk/test/unit/test_mozTXTToHTMLConv.js @@ -0,0 +1,395 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test that mozITXTToHTMLConv works properly. + */ + +"use strict"; + +function run_test() { + let converter = Cc["@mozilla.org/txttohtmlconv;1"].getService( + Ci.mozITXTToHTMLConv + ); + + const scanTXTtests = [ + // -- RFC1738 + { + input: "RFC1738: then", + url: "http://mozilla.org", + }, + { + input: "RFC1738: then", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- RFC2396E + { + input: "RFC2396E: then", + url: "http://mozilla.org/", + }, + { + input: "RFC2396E: then", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- abbreviated + { + input: "see www.mozilla.org maybe", + url: "http://www.mozilla.org", + }, + { + input: "mail john.doe+test@mozilla.org maybe", + url: "mailto:john.doe+test@mozilla.org", + }, + // -- delimiters + { + input: "see http://www.mozilla.org/maybe today", // Spaces + url: "http://www.mozilla.org/maybe", + }, + { + input: 'see "http://www.mozilla.org/maybe today"', // Double quotes + url: "http://www.mozilla.org/maybetoday", // spaces ignored + }, + { + input: "see ", // Angle brackets + url: "http://www.mozilla.org/maybetoday", // spaces ignored + }, + // -- freetext + { + input: "I mean http://www.mozilla.org/.", + url: "http://www.mozilla.org/", + }, + { + input: "you mean http://mozilla.org:80, right?", + url: "http://mozilla.org:80", + }, + { + input: "go to http://mozilla.org; then go home", + url: "http://mozilla.org", + }, + { + input: "http://mozilla.org! yay!", + url: "http://mozilla.org", + }, + { + input: "er, http://mozilla.com?", + url: "http://mozilla.com", + }, + { + input: "http://example.org- where things happen", + url: "http://example.org", + }, + { + input: "see http://mozilla.org: front page", + url: "http://mozilla.org", + }, + { + input: "'http://mozilla.org/': that's the url", + url: "http://mozilla.org/", + }, + { + input: "some special http://mozilla.org/?x=.,;!-:x", + url: "http://mozilla.org/?x=.,;!-:x", + }, + { + // escape & when producing html + input: "'http://example.org/?test=true&success=true': ok", + url: "http://example.org/?test=true&success=true", + }, + { + input: "bracket: http://localhost/[1] etc.", + url: "http://localhost/", + }, + { + input: "bracket: john.doe+test@mozilla.org[1] etc.", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "parenthesis: (http://localhost/) etc.", + url: "http://localhost/", + }, + { + input: "parenthesis: (john.doe+test@mozilla.org) etc.", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "(thunderbird)http://mozilla.org/thunderbird", + url: "http://mozilla.org/thunderbird", + }, + { + input: "(mail)john.doe+test@mozilla.org", + url: "mailto:john.doe+test@mozilla.org", + }, + { + input: "()http://mozilla.org", + url: "http://mozilla.org", + }, + { + input: + "parenthesis included: http://kb.mozillazine.org/Performance_(Thunderbird) etc.", + url: "http://kb.mozillazine.org/Performance_(Thunderbird)", + }, + { + input: "parenthesis slash bracket: (http://localhost/)[1] etc.", + url: "http://localhost/", + }, + { + input: "parenthesis bracket: (http://example.org[1]) etc.", + url: "http://example.org", + }, + { + input: "ipv6 1: https://[1080::8:800:200C:417A]/foo?bar=x test", + url: "https://[1080::8:800:200C:417A]/foo?bar=x", + }, + { + input: "ipv6 2: http://[::ffff:127.0.0.1]/#yay test", + url: "http://[::ffff:127.0.0.1]/#yay", + }, + { + input: "ipv6 parenthesis port: (http://[2001:db8::1]:80/) test", + url: "http://[2001:db8::1]:80/", + }, + { + input: + "test http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2 test", + url: + "http://www.map.com/map.php?t=Nova_Scotia&markers=//Not_a_survey||description=plm2", + }, + { + input: "bug#1509493 (john@mozilla.org)john@mozilla.org test", + url: "mailto:john@mozilla.org", + text: "john@mozilla.org", + }, + { + input: "bug#1509493 {john@mozilla.org}john@mozilla.org test", + url: "mailto:john@mozilla.org", + text: "john@mozilla.org", + }, + ]; + + const scanTXTglyph = [ + // Some "glyph" testing (not exhaustive, the system supports 16 different + // smiley types). + { + input: "this is superscript: x^2", + results: [""], + }, + { + input: "this is plus-minus: +/-", + results: ["±"], + }, + { + input: "this is a smiley :)", + results: ["moz-smiley-s1"], + }, + { + input: "this is a smiley :-)", + results: ["moz-smiley-s1"], + }, + { + input: "this is a smiley :-(", + results: ["moz-smiley-s2"], + }, + ]; + + const scanTXTstrings = [ + "underline", // ASCII + "äöüßáéíóúî", // Latin-1 + "a\u0301c\u0327c\u030Ce\u0309n\u0303t\u0326e\u0308d\u0323", + // áçčẻñțëḍ Latin + "\u016B\u00F1\u0257\u0119\u0211\u0142\u00ED\u00F1\u0119", + // Pseudo-ese ūñɗęȑłíñę + "\u01DDu\u0131\u0283\u0279\u01DDpun", // Upside down ǝuıʃɹǝpun + "\u03C5\u03C0\u03BF\u03B3\u03C1\u03AC\u03BC\u03BC\u03B9\u03C3\u03B7", + // Greek υπογράμμιση + "\u0441\u0438\u043B\u044C\u043D\u0443\u044E", // Russian сильную + "\u0C2C\u0C32\u0C2E\u0C46\u0C56\u0C28", // Telugu బలమైన + "\u508D\u7DDA\u3059\u308B", // Japanese 傍線する + "\uD841\uDF0E\uD841\uDF31\uD841\uDF79\uD843\uDC53\uD843\uDC78", + // Chinese (supplementary plane) + "\uD801\uDC14\uD801\uDC2F\uD801\uDC45\uD801\uDC28\uD801\uDC49\uD801\uDC2F\uD801\uDC3B", + // Deseret 𐐔𐐯𐑅𐐨𐑉𐐯𐐻 + ]; + + const scanTXTstructs = [ + { + delimiter: "/", + tag: "i", + class: "moz-txt-slash", + }, + { + delimiter: "*", + tag: "b", + class: "moz-txt-star", + }, + { + delimiter: "_", + tag: "span", + class: "moz-txt-underscore", + }, + { + delimiter: "|", + tag: "code", + class: "moz-txt-verticalline", + }, + ]; + + const scanHTMLtests = [ + { + input: "http://foo.example.com", + shouldChange: true, + }, + { + input: " foo", + shouldChange: false, + }, + { + input: "see http://abbr.example.com", + shouldChange: true, + }, + { + input: "", + shouldChange: false, + }, + { + input: "", + shouldChange: false, + }, + { + input: "", + shouldChange: false, + }, + { + input: + "", + shouldChange: false, + }, + { + input: "", + shouldChange: false, + }, + { + input: "", + shouldChange: false, + }, + { + input: "http://head.example.com/", + shouldChange: false, + }, + { + input: "
see http://header.example.com
", + shouldChange: true, + }, + { + input: "